3D Graphic
Orillusion
provides an extension package @orillusion/graphic for rendering dynamically changing points, lines, surfaces, and volumes. This package allows for the creation of dynamic meshes using specific methods, which are efficiently managed and integrated into the engine's rendering pipeline. It is both high-performance and user-friendly.
Three main modules are available for creating high-performance graphic data:
Graphic3D
: Provides basic line drawing capabilities, commonly used for drawing auxiliary lines. Graphic3DMesh
Renderer: Allows batch creation of Mesh
clones in a single renderer, with the ability to freely adjust each clone's Transform
, Texture
, and Material
, offering high flexibility in creating graphics and animations. Shape3D
Renderer: Creates complex custom Shape3D
objects, such as EllipseShape3D
, RoundRectShape3D
, CircleShape3D
, etc. For Shape3D
objects with continuous drawing capabilities, such as Path2DShape3D
and Path3DShape3D
, the design references the CanvasPath API, allowing developers to use familiar methods for graphic rendering.
Installation
Like the engine itself, the graphic plugins can be introduced using NPM
or CDN
links:
1. Installing via NPM
Packages
npm install @orillusion/core --save
npm install @orillusion/graphic --save
import { Engine3D } from "@orillusion/core"
import { Graphic3D, Shape3D } from "@orillusion/graphic"
2. Installing via CDN
links
It is recommended to use the ESModule
build version
<script type="module">
import { Engine3D } from "https://unpkg.com/@orillusion/core/dist/orillusion.es.js"
import { Graphic3D, Shape3D } from "https://unpkg.com/@orillusion/graphic/dist/graphic.es.js"
</script>
Or, load the UMD
build version using <script>
, accessing the Shape3D
module from the global Orillusion
variable:
<script src="https://unpkg.com/@orillusion/core/orillusion.umd.js"></script>
<script src="https://unpkg.com/@orillusion/stats/dist/graphic.umd.js"></script>
<script>
const { Engine3D, Graphic3D, Shape3D } = Orillusion
</script>
Graphic3D
Create a Graphic3D
object to uniformly draw graphics in the scene. Currently, three APIs
are provided for quick creation of different line combinations: drawLines
, drawBox
, and drawCircle
.
Basic Methods
import {Graphic3D} from '@orillusion/graphic'
// ...
// Create a Graphic3D object
let graphic3D = new Graphic3D();
// Add to the scene
scene.addChild(graphic3D);
// Use graphic3D to uniformly draw lines
// line - (uid, [start1, end1, start2, end2, ...], color)
graphic3D.drawLines('line', [new Vector3(0, 0, 0), new Vector3(0, 10, 0)], new Color(1, 0, 0));
// box - (uid, center, size, color)
graphic3D.drawBox('box', new Vector3(-5, -5, -5), new Vector3(5, 5, 5), new Color(0, 1, 0));
// circle - (uid, center, radius, segments, up, color)
graphic3D.drawCircle('circle', new Vector3(-15, -5, -5), 5, 15, Vector3.X_AXIS, new Color(0, 0, 1));
import { Object3D, Scene3D, Engine3D, Vector3, Color, AnimationCurve, Keyframe, View3D, AtmosphericComponent, CameraUtil, HoverCameraController, DirectLight, KelvinUtil } from '@orillusion/core';
import { Graphic3D } from '@orillusion/graphic';
import { Stats } from '@orillusion/stats';
class GraphicLine {
scene: Scene3D;
view: View3D;
graphic3D: Graphic3D;
async run() {
await Engine3D.init();
// init Scene3D
this.scene = new Scene3D();
this.scene.exposure = 1;
this.scene.addComponent(Stats);
// init sky
let atmosphericSky: AtmosphericComponent;
atmosphericSky = this.scene.addComponent(AtmosphericComponent);
atmosphericSky.exposure = 1.0;
// init Camera3D
let camera = CameraUtil.createCamera3DObject(this.scene);
camera.perspective(60, Engine3D.aspect, 1, 5000);
// init Camera Controller
let hoverCtrl = camera.object3D.addComponent(HoverCameraController);
hoverCtrl.setCamera(-30, -15, 100);
// init View3D
let view = new View3D();
view.scene = this.scene;
view.camera = camera;
// add a Graphic3D
this.graphic3D = new Graphic3D();
this.scene.addChild(this.graphic3D);
// create direction light
let lightObj3D = new Object3D();
lightObj3D.x = 0;
lightObj3D.y = 30;
lightObj3D.z = -40;
lightObj3D.rotationX = 20;
lightObj3D.rotationY = 160;
lightObj3D.rotationZ = 0;
let light = lightObj3D.addComponent(DirectLight);
light.lightColor = KelvinUtil.color_temperature_to_rgb(5355);
light.intensity = 30;
this.scene.addChild(light.object3D);
// relative light to sky
atmosphericSky.relativeTransform = light.transform;
Engine3D.startRenderView(view);
this.view = view;
await this.initScene();
}
async initScene() {
this.graphic3D.drawLines('line1', [Vector3.ZERO, new Vector3(0, 10, 0)], new Color().hexToRGB(Color.RED));
let animCurve = new AnimationCurve();
animCurve.addKeyFrame(new Keyframe(0, 0.5));
animCurve.addKeyFrame(new Keyframe(0.15, -0.2));
animCurve.addKeyFrame(new Keyframe(0.22, 0.4));
animCurve.addKeyFrame(new Keyframe(0.34, 0.2));
animCurve.addKeyFrame(new Keyframe(0.65, -0.2));
animCurve.addKeyFrame(new Keyframe(1, 0.9));
let lines: Vector3[] = [];
for (let i = 0; i < 100; i++) {
let y = animCurve.getValue(i / (100 - 1)) * 10;
lines.push(new Vector3(i, y, 0));
}
this.graphic3D.drawLines('line2', lines, new Color().hexToRGB(Color.RED));
this.graphic3D.drawBox('box1', new Vector3(-5, -5, -5), new Vector3(5, 5, 5), new Color().hexToRGB(Color.GREEN));
this.graphic3D.drawCircle('Circle1', new Vector3(-15, -5, -5), 5, 15, Vector3.X_AXIS, new Color().hexToRGB(Color.GREEN));
this.graphic3D.drawCircle('Circle2', new Vector3(-15, -5, -5), 5, 15, Vector3.Y_AXIS, new Color().hexToRGB(Color.GREEN));
this.graphic3D.drawCircle('Circle3', new Vector3(-15, -5, -5), 5, 15, Vector3.Z_AXIS, new Color().hexToRGB(Color.GREEN));
}
}
new GraphicLine().run();
Graphic3DMesh
Renderer
By Graphic3DMesh.draw()
, we can create an instance of Graphic3DMeshRenderer
, This object can be viewed as a collection of multiple cloned Geometry
objects. For each object in this collection, you can set the position and texture to achieve the desired visual effect.
Parameter | Description |
---|---|
scene | which scene to add the renderer |
geo | which geometry to clone |
texture | the index of a given texture array |
count | maximum number of clones in the collection that a renderer can support (choosing an appropriate value will improve performance) |
TIP
The geo
parameter typically uses a simple PlaneGeometry
as the model source, with different textures applied to create various appearances. In theory, you can use any model source to create diverse effects. For example, by using a BoxGeometry
model, you can create graphics composed of many cubes, enabling the creation of pixel art-style scenes or simulating voxel rendering.
Modifying
Transform
: To modify the rotation, scale, or position of a specific unit at a given index, access theobject3Ds
belonging to theGraphic3DMeshRenderer
. Use the index to obtain the correspondingObject3D
and adjust itsTransform
. This change will be synchronized with the target unit.Modifying
Texture
: Call the functionsetTextureID
to specify and modify the texture index for a particular unit at a given index. The textures are sourced from the textures array provided in the initialization parameters of theGraphic3DMeshRenderer
.Modifying
Material
: TheGraphic3DMeshRenderer
class provides a series of APIs with names similar tosetTextureID
. The first parameter specifies the target unit, while the second parameter sets the relevant properties. Developers can use these APIs to modify various aspects of the graphics, such asColor
,UV
,Emissive
properties, and more.
Example
import { Object3D, Scene3D, Engine3D, BitmapTexture2DArray, BitmapTexture2D, PlaneGeometry, Vector3, Matrix4, Time, BlendMode, Color, ColorUtil } from "@orillusion/core";
import { Graphic3D, Graphic3DMesh, Graphic3DMeshRenderer } from '@orillusion/graphic';
// Load textures
let textureArray = [];
textureArray.push(await Engine3D.res.loadTexture("path/to/texture.png") as BitmapTexture2D);
let bitmapTexture2DArray = new BitmapTexture2DArray(textureArray[0].width, textureArray[0].height, textureArray.length);
bitmapTexture2DArray.setTextures(textureArray);
// take a plane as the clone sorce
let geometry = new PlaneGeometry(1, 1, 1, 1, Vector3.Z_AXIS);
// Create a Graphic3DMeshRenderer instance with maxium 100 clones
let mr:Graphic3DMeshRenderer = Graphic3DMesh.draw(scene, geometry, bitmapTexture2DArray, 100);
// set material properties
mr.material.blendMode = BlendMode.ADD;
mr.material.transparent = true;
mr.material.depthWriteEnabled = false;
mr.material.useBillboard = true;
// Get the corresponding object3Ds and modify the Transform property of that Object3D to synchronously update the Transform of the target clone
// By placing the same operation in the main update function of the engine, you can modify it every frame to drive the animation effect
let parts = mr.object3Ds;
for (let i = 0; i < 100; i++) {
const element = parts[i];
// set texture index from textureArray
mr.setTextureID(i, 0);
// update transform
element.transform.x = 1;
element.transform.scaleX = 1;
element.transform.rotationX = 0;
// ...
}
import { Object3D, Scene3D, Engine3D, AtmosphericComponent, CameraUtil, HoverCameraController, View3D, UnLitTexArrayMaterial, BitmapTexture2DArray, BitmapTexture2D, PlaneGeometry, Vector3, Matrix4, Time, BlendMode, Color } from '@orillusion/core';
import { Stats } from '@orillusion/stats';
import { Graphic3DMesh, Graphic3DMeshRenderer } from '@orillusion/graphic';
class Sample_GraphicMesh {
private scene: Scene3D;
private parts: Object3D[];
private width: number;
private height: number;
private cafe: number = 47;
private view: View3D;
graphicMeshRenderer: Graphic3DMeshRenderer;
constructor() {}
async run() {
Matrix4.maxCount = 500000;
Matrix4.allocCount = 500000;
await Engine3D.init({beforeRender: ()=> this.update()});
Engine3D.setting.render.debug = true;
Engine3D.setting.shadow.shadowBound = 5;
this.scene = new Scene3D();
this.scene.addComponent(Stats);
let sky = this.scene.addComponent(AtmosphericComponent);
sky.enable = false;
let camera = CameraUtil.createCamera3DObject(this.scene);
camera.perspective(60, Engine3D.aspect, 1, 5000.0);
camera.object3D.addComponent(HoverCameraController).setCamera(30, 0, 120);
this.view = new View3D();
this.view.scene = this.scene;
this.view.camera = camera;
Engine3D.startRenderView(this.view);
await this.initScene();
}
async initScene() {
let texts:any[] = [];
texts.push((await Engine3D.res.loadTexture('https://cdn.orillusion.com/textures/128/star_0008.png')) as BitmapTexture2D);
let bitmapTexture2DArray = new BitmapTexture2DArray(texts[0].width, texts[0].height, texts.length);
bitmapTexture2DArray.setTextures(texts);
let mat = new UnLitTexArrayMaterial();
mat.baseMap = bitmapTexture2DArray;
mat.name = 'LitMaterial';
{
this.width = 15;
this.height = 15;
let geometry = new PlaneGeometry(1, 1, 1, 1, Vector3.Z_AXIS);
this.graphicMeshRenderer = Graphic3DMesh.draw(this.scene, geometry, bitmapTexture2DArray, this.width * this.height);
this.parts = this.graphicMeshRenderer.object3Ds;
this.graphicMeshRenderer.material.blendMode = BlendMode.ADD;
this.graphicMeshRenderer.material.transparent = true;
this.graphicMeshRenderer.material.depthWriteEnabled = false;
this.graphicMeshRenderer.material.useBillboard = true;
for (let i = 0; i < this.width * this.height; i++) {
const element = this.parts[i];
this.graphicMeshRenderer.setTextureID(i, 0);
element.transform.scaleX = 5.5;
element.transform.scaleY = 5.5;
element.transform.scaleZ = 5.5;
}
}
}
update(){
if (this.parts) {
let len = this.parts.length;
for (let i = 0; i < len; i++) {
const element = this.parts[i];
let tmp = this.sphericalFibonacci(i, len);
tmp.scaleBy(Math.sin(i + Time.frame * 0.01) * this.cafe);
element.transform.localPosition = tmp;
}
}
}
public madfrac(A: number, B: number): number {
return A * B - Math.floor(A * B);
}
public sphericalFibonacci(i: number, n: number): Vector3 {
const PHI = Math.sqrt(5.0) * 0.5 + 0.5;
let phi = 2.0 * Math.PI * this.madfrac(i, PHI - 1);
let cosTheta = 1.0 - (2.0 * i + 1.0) * (1.0 / n);
let sinTheta = Math.sqrt(Math.max(Math.min(1.0 - cosTheta * cosTheta, 1.0), 0.0));
return new Vector3(Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta);
}
}
new Sample_GraphicMesh().run();
See more
Graphic3D
API usage in Graphic3D
Shape3D
Renderer
By Shape3DMaker
, we can create a Shape3DRenderer
renderer,which can hold and maintain a dataset of Shape3D
objects. Each Shape3D corresponds to a variety of predefined shapes, such as EllipseShape3D
、RoundRectShape3D
、CircleShape3D
and so on. Additionally, Path2DShape3D
and Path3DShape3D
offer a more extensive API that can assist you in combining and drawing complex graphics.
Parameter | Description |
---|---|
name | name to identify Shape3DRenderer |
scene | which scene to add the renderer |
textureList | the index of a given texture array |
maxNodeCount | maximum number of nodes in the collection that a renderer can support |
triangleEachNode | how many triangles to draw for each node |
The renderer is designed based on the API of
CanvasPath
, allowing developers to continue using familiar development practices while working with theOrillusion
engine for 3D graphics rendering. The 2D drawing section of the renderer refers to drawing points, lines, and surfaces in the XZ plane. Each unit can still be independently controlled viaTransform
. For drawing shapes in 3D space, you need to usePath3DShape3D
to begin drawing graphics that incorporate Y-axis elevation data.
Properties
The following table provides a brief summary and description of the properties of Shape3D
.
Property | Description |
---|---|
lineColor | The color additive when drawing lines |
fillColor | The color additive when drawing filled areas |
lineTextureID | Sets the texture used when drawing lines |
fillTextureID | Sets the texture used when filling areas |
fillRotation | Sets the rotation angle for textures used in filled areas |
shapeOrder | Sets the layering of each shape (to eliminate z-fighting; each Shape3DRenderer can define the maximum range for z-fighting, and based on this range and the number of Shape3D instances, calculates the offset for each Shape3D ) |
points3D | A placeholder for externally provided key points collection |
isClosed | Indicates whether the shape is closed (starts and ends at the same point) |
fill | Indicates whether the shape is filled |
lineWidth | The width of the line when drawn |
lineUVRect | UV data: xy correspond to the offset of the line texture, and zw correspond to the scaling of the texture data |
fillUVRect | UV data: xy correspond to the offset of the fill area texture, and zw correspond to the scaling of the texture data |
uvSpeed | UV data: xy correspond to the movement speed of the UVs for the fill area texture; zw corresponds to the movement speed of the UVs for the line texture. |
Shapes
Like the CanvasPath
API, we have provided some subclasses of Shape3D
for users to draw specific shapes:
Shape | Description |
---|---|
CircleShape3D | Circle and arc shapes |
CurveShape3D | Bezier curve controlled by two anchor points |
EllipseShape3D | Elliptical shapes |
LineShape3D | Polyline shapes |
Path2DShape3D | Draws line paths on the XZ plane |
Path3DShape3D | Draws line paths in 3D space |
QuadraticCurveShape3D | Bezier curve controlled by one anchor point |
RoundRectShape3D | Rectangle and rounded rectangle shapes |
Methods
All instances from Shape3DMaker
could create shapes by the following methods:
Method | Shape |
---|---|
ellipse | EllipseShape3D |
arc | CircleShape3D |
line | LineShape3D |
quadraticCurve | QuadraticCurveShape3D |
curve | CurveShape3D |
path2D | Path2DShape3D |
path3D | Path3DShape3D |
rect | RoundRectShape3D |
roundRect | RoundRectShape3D |
TIP
All 2D shapes, e.g. Path2D
, will ignore the y-axis elevation data, and will be drawn in the XZ
plane.
Additionally, we could create/delete Shape3D
from Shape3DRenderer
:
Methods | Description |
---|---|
createShape | create a new Shape3D instance |
removeShape | delete a Shape3D instance |
getShapeObject3D | get the Object3D instance of a shape3D |
Usage
import { Object3D, Scene3D, Engine3D, BitmapTexture2DArray, BitmapTexture2D, PlaneGeometry, Vector3, Matrix4, Time, BlendMode, Color,ColorUtil } from "@orillusion/core";
import { CircleShape3D, EllipseShape3D, Shape3DMaker, Shape3D } from "@orillusion/graphic";
// load textures
let textureArray = [];
textureArray.push(await Engine3D.res.loadTexture("path/to/texture.png") as BitmapTexture2D);
let bitmapTexture2DArray = new BitmapTexture2DArray(textureArray[0].width, textureArray[0].height, textureArray.length);
bitmapTexture2DArray.setTextures(textureArray);
// create a Shape3DRenderer in the scene with a texture array
maker = Shape3DMaker.makeRenderer(`path`, bitmapTexture2DArray, scene);
maker.renderer.material.doubleSide = true;
// create a Circle shape with radius 5 at center (0, 0)
let circle:CircleShape3D = maker.arc(5, 0, 0);
circle.lineWidth = 1; // width of line
circle.segment = 16; // the segment of circle
circle.fill = true; // if fill the circle
circle.line = true; // if draw a line border
circle.uvSpeed = new Vector4(0, 0, 0, Math.random() - 0.5).multiplyScalar(0.005); // set UV speed
circle.fillColor = Color.randomRGB(); // set a fill color
circle.lineColor = Color.randomRGB(); // set a border color
circle.startAngle = 30; // set a start angle
circle.endAngle = 240; // set the end angle
// we could tween a animation of the circle by updating properties in the main loop
The above code demonstrates how to draw an independent circle/arc by creating an instance of
CircleShape3D
. You can also achieve this by creating a genericPath2DShape3D
instance and then calling itsarc()
function.
import { Object3D, Scene3D, Engine3D, AtmosphericComponent, CameraUtil, HoverCameraController, View3D, DirectLight, KelvinUtil, BitmapTexture2DArray, BitmapTexture2D, Matrix4, Color, LineJoin, Vector4, Object3DUtil, AxisObject } from '@orillusion/core';
import { Stats } from '@orillusion/stats';
import { Shape3DMaker, Shape3D, CircleArcType, CircleShape3D } from '@orillusion/graphic';
import * as dat from 'dat.gui';
/**
* This example shows how to use Shape2D to draw various different paths on xz plane.
*
* @export
* @class Sample_Shape3DPath2D
*/
class Sample_Shape3DPath2D {
lightObj3D: Object3D;
scene: Scene3D;
view: View3D;
async run() {
Matrix4.maxCount = 10000;
Matrix4.allocCount = 10000;
await Engine3D.init({ beforeRender: () => this.update() });
Engine3D.setting.render.debug = true;
Engine3D.setting.shadow.shadowBound = 5;
this.scene = new Scene3D();
this.scene.addComponent(Stats);
let sky = this.scene.addComponent(AtmosphericComponent);
let camera = CameraUtil.createCamera3DObject(this.scene);
camera.perspective(60, Engine3D.aspect, 1, 5000.0);
camera.object3D.addComponent(HoverCameraController).setCamera(0, -60, 60);
this.view = new View3D();
this.view.scene = this.scene;
this.view.camera = camera;
Engine3D.startRenderView(this.view);
await this.initScene();
this.scene.addChild(new AxisObject(10, 0.1));
sky.relativeTransform = this.lightObj3D.transform;
}
async initScene() {
{
/******** light *******/
this.lightObj3D = new Object3D();
this.lightObj3D.rotationX = 21;
this.lightObj3D.rotationY = 108;
this.lightObj3D.rotationZ = 10;
let directLight = this.lightObj3D.addComponent(DirectLight);
directLight.lightColor = KelvinUtil.color_temperature_to_rgb(5355);
directLight.castShadow = false;
directLight.intensity = 20;
this.scene.addChild(this.lightObj3D);
await this.addNode();
}
{
let floor = Object3DUtil.GetSingleCube(100, 0.1, 100, 0.2, 0.2, 0.2);
floor.y = -0.2;
this.scene.addChild(floor);
}
}
private maker: Shape3DMaker;
private async addNode() {
let textureArray:any[] = [];
textureArray.push((await Engine3D.res.loadTexture('https://cdn.orillusion.com/textures/128/vein_0013.png')) as BitmapTexture2D);
textureArray.push((await Engine3D.res.loadTexture('https://cdn.orillusion.com/textures/128/vein_0014.png')) as BitmapTexture2D);
let bitmapTexture2DArray = new BitmapTexture2DArray(textureArray[0].width, textureArray[0].height, textureArray.length);
bitmapTexture2DArray.setTextures(textureArray);
this.maker = Shape3DMaker.makeRenderer(`path`, bitmapTexture2DArray, this.scene);
this.maker.renderer.material.doubleSide = true;
this.createPath();
}
private createPath(): Shape3D {
let circle = this.maker.arc(20, 0, 360, undefined);
circle.lineWidth = 2;
circle.segment = 40;
circle.fill = true;
circle.line = true;
circle.isClosed = false;
circle.lineUVRect.z = 0.5;
circle.lineUVRect.w = 0.5;
circle.fillUVRect.z = 0.1;
circle.fillUVRect.w = 0.1;
circle.fillTextureID = 0;
circle.lineTextureID = 1;
circle.lineColor = Color.random();
circle.uvSpeed = new Vector4(0, 0, 0, Math.random() - 0.5).multiplyScalar(0.005);
const GUIHelp = new dat.GUI();
this.renderCircle(GUIHelp, circle, 5, false);
return circle;
}
update() {}
renderCircle(GUIHelp: dat.GUI, shape: CircleShape3D, maxSize: number, open: boolean = true, name?: string) {
name ||= 'Circle3D_' + shape.shapeIndex;
GUIHelp.addFolder(name);
GUIHelp.add(shape, 'radius', 0, maxSize, 0.1);
GUIHelp.add(shape, 'segment', 0, 100, 1);
GUIHelp.add(shape, 'startAngle', 0, 360, 1);
GUIHelp.add(shape, 'endAngle', 0, 360, 1);
let arcType = {};
arcType['sector'] = CircleArcType.Sector;
arcType['moon'] = CircleArcType.Moon;
GUIHelp.add({ arcType: shape.arcType }, 'arcType', arcType).onChange((v) => {
shape.arcType = Number.parseInt(v);
});
this.renderCommonShape3D(GUIHelp, shape, maxSize);
}
renderCommonShape3D(GUIHelp: dat.GUI, shape: Shape3D, maxSize: number, uvMin: number = 0.01, uvMax: number = 1.0) {
GUIHelp.add(shape, 'line');
GUIHelp.add(shape, 'fill');
GUIHelp.add(shape, 'isClosed');
GUIHelp.add(shape, 'lineWidth', 0, maxSize, 0.01);
GUIHelp.add(shape, 'fillRotation', -Math.PI, Math.PI, 0.01);
this.renderVec4(GUIHelp, 'FillUVRect.', shape, 'fillUVRect', 0, 10, 0.01);
this.renderVec4(GUIHelp, 'LineUVRect.', shape, 'lineUVRect', 0, 10, 0.01);
this.renderVec4(GUIHelp, 'UVSpeed.', shape, 'uvSpeed', -0.01, 0.01, 0.0001);
GUIHelp.add(shape, 'lineTextureID', 0, 1, 1);
GUIHelp.add(shape, 'fillTextureID', 0, 1, 1);
}
renderVec4(GUIHelp: dat.GUI, label: string, target: Object, key: string, min: number, max: number, step: number = 0.01) {
let components = ['x', 'y', 'z', 'w'];
let data = {} as any;
let vec4: Vector4 = target[key];
for (let component of components) {
data[label + component] = vec4[component];
GUIHelp.add(data, label + component, min, max, step).onChange((v) => {
vec4[component] = v;
target[key] = vec4;
});
}
}
}
new Sample_Shape3DPath2D().run();
See more
Shape3D
API usage in Shape3D