Skip to content

About

A lightweight deferred rendering WebGL 2.0 framework in JavaScript.

Resources

Stars

14 stars

Watchers

6 watching

Forks

Latest commit

 

History

194 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RenderCore

A lightweight deffered rendering WebGL 2.0 framework in JavaScript.

Basic example

The example code below show a simple way how to set up a scene for rendering a triangle.

const canvas = new RC.Canvas(document.body);

const renderer = new RC.MeshRenderer(canvas, RC.WEBGL2);
renderer.addShaderLoaderUrls(shaderLocation);

const scene = new RC.Scene();
const camera = new RC.OrthographicCamera(-1, 1, 1, -1, 0, 10);

const geometry = new RC.Geometry({vertices: new RC.Float32Attribute([-1, -1, 0, 1, -1, 0, 0, 1, 0], 3)});
const material = new RC.MeshBasicMaterial();
const object = new RC.Mesh(geometry, material);
scene.add(object);

The example shows some of the shapes awailable in the framework, how to create, color, and render them.

//mesh (point)
const pointGeometry = new RC.Geometry({vertices: new RC.Float32Attribute([0, 0, 0], 3)});
const point = new RC.Point(pointGeometry);
point.material.pointSize = 64.0;
...

//mesh (line)
const lineGeometry = new RC.Geometry({vertices: new RC.Float32Attribute([-1, -1, 0, 1, 1, 0], 3)});
const line = new RC.Line(lineGeometry);
...

//mesh (triangle)
const triangleGeometry = new RC.Geometry({vertices: new RC.Float32Attribute([-1, -1, 0, 1, -1, 0, 0, 1, 0], 3)});
const triangle = new RC.Mesh(triangleGeometry);
triangle.material.side = RC.FRONT_AND_BACK_SIDE;
...

//circle
const circle = new RC.Circle();
...

//quad
const quad = new RC.Quad({x: -1, y: 1}, {x: 1, y: -1});
quad.material.side = RC.FRONT_AND_BACK_SIDE;
...

//cube
const cube = new RC.Cube(1.0, new RC.Color());
...

The example shows how to load an OBJ model and render it with a texture.

The example demonstrates how to use ambient and directional light. The example also shows how to link keyboard interaction.

...
// Initialize lights and add them to the scene
aLight = new RC.AmbientLight(new RC.Color("#FFFFFF"), 0.1);
scene.add(aLight);

dLight = new RC.DirectionalLight(new RC.Color("#FF0000"), 1.0);
dLight.position = new RC.Vector3(1.0, 0.39, 0.7);
scene.add(dLight);
...

This example demonstrates how to use different types lights.

const aLight = new RC.AmbientLight(new RC.Color("#FFFFFF"), 0.1);
scene.add(aLight);

const dLight = new RC.DirectionalLight(new RC.Color("#FF0000"), 0.5);
dLight.rotation = new RC.Euler(-1.3, -1.2, 0.0);
scene.add(dLight);

const pLight = new RC.PointLight(new RC.Color("#0000FF"), 0.5);
pLight.position = new RC.Vector3(0.0, 1.0, 1.0);
scene.add(pLight);

const sLight = new RC.SpotLight(new RC.Color("#FFFFFF"), 0.5);
sLight.position = new RC.Vector3(0.0, 1.5, 0.0);
sLight.rotation = new RC.Euler(-Math.PI/2, 0.0, 0.0);
scene.add(sLight);

The example demonstrates how to use ambient and directional light. The example also shows how to link keyboard interaction.

...
// Initialise scenes first
this.initScenes();

// Initialize render Queue
this.renderQueue = this.initRenderQueue();

...
// Example of RenderQueue initialization function
initRenderQueue() {
  // Create first pass
  this.firstPass = new RC.RenderPass(
    RC.RenderPass.BASIC,
    (textureMap, additionalData) => {},
    (textureMap, additionalData) => {
      return {scene: this.firstScene, camera: this.firstCamera};
    },
    (textureMap, additionalData) => {},
    RC.RenderPass.TEXTURE,
    {width: 512, height: 512},
    "depthTexture",
    [{
      id: "color",
      textureConfig: RC.RenderPass.DEFAULT_RGBA_TEXTURE_CONFIG
    }]
  );

  // Create second pass
  this.secondPass = new RC.RenderPass(
    RC.RenderPass.BASIC,
    (textureMap, additionalData) => {
      this.quad.material.clearMaps();
      this.quad.material.addMap(textureMap.color);
    },
    (textureMap, additionalData) => {
      return {scene: this.secondScene, camera: this.secondCamera};
    },
    (textureMap, additionalData) => {},
    RC.RenderPass.SCREEN,
    {width: this.canvas.width, height: this.canvas.height}
  );

  // Create a RenderQueue and add both render passes to it
  let renderQueue = new RC.RenderQueue(this.renderer);
  renderQueue.pushRenderPass(this.firstPass);
  renderQueue.pushRenderPass(this.secondPass);

  return renderQueue;
}

Here we demonstrates a technique to get a unique identifier of a clicked rendered object.

const canvas = new RC.Canvas(document.body);

const renderer = new RC.MeshRenderer(canvas, RC.WEBGL2);
renderer.addShaderLoaderUrls(shaderLocation);
const renderQueue = new RC.RenderQueue(renderer);

const scene = new RC.Scene();
const camera = new RC.PerspectiveCamera(75, canvas.width/canvas.height, 0.125, 128);

const picker = new RC.PickerFX(
    renderer, 
    {scene: scene, camera: camera},
    {},
    new RC.FX.output("color_picker")
);
renderQueue.pushRenderQueue(picker);

const MainPass = new RC.RenderPass(
    // Rendering pass type
    RC.RenderPass.BASIC,
    // Initialize function
    (textureMap, additionalData) => { },
    // Preprocess function
    (textureMap, additionalData) => { return { scene: scene, camera: camera }; },
    // PostprocesWs function
    (textureMap, additionalData) => { },
    // Target
    RC.RenderPass.SCREEN,
    // Viewport
    { width: canvas.width, height: canvas.height },
    // Bind depth texture to this ID
    "depth_main",
    // Outputs
    [{id: "color_main", textureConfig: RC.RenderPass.DEFAULT_RGBA_TEXTURE_CONFIG}]
);
renderQueue.pushRenderPass(MainPass);

This example show how to draw a lot of models with same set of vertex data.

const geometry = new RC.Geometry();
geometry.vertices = new RC.Float32Attribute(cubeVertices, 3);
geometry.vertices.divisor = 0;
geometry.computeVertexNormals();
geometry.normals.divisor = 0;

geometry.MMat = new RC.Float32Attribute(instances.reduce((acc, x) => {acc.push(...x.matrix.elements); return acc;}, new Array()), 16);
geometry.MMat.divisor = 1;
geometry.MMat.drawType = RC.BufferAttribute.DRAW_TYPE.DYNAMIC;

const material = new RC.MeshBasicMaterial();
cubeMaterial.emissive.set(0.0, 0.0, 0.0);

const object = new RC.Mesh(geometry, material);
object.position.set(0.0, 0.0, 0.0);
object.scale.set(0.1, 0.1, 0.1);
object.instanced = true;
object.instanceCount = instances.length;

scene.add(cubeObject);

This example show how to draw a lot of models with same set of vertex data.

const spriteGeometry = new RC.SpriteGeometry({baseGeometry: spriteBaseGeometry});
spriteGeometry.vertices.divisor = 0;
spriteGeometry.normals.divisor = 0;

spriteGeometry.translation = new RC.Float32Attribute(spriteTranslations.reduce((acc, x) => {acc.push(...x.toArray()); return acc;}, new Array()), 4);
spriteGeometry.translation.divisor = 1;
spriteGeometry.translation.drawType = RC.BufferAttribute.DRAW_TYPE.DYNAMIC;

const spriteMaterial = new RC.SpriteBasicMaterial({baseGeometry: spriteBaseGeometry, color: new RC.Color(Math.random(), Math.random(), Math.random()), spriteSize: [8, 8], drawCircles: true});
spriteMaterial.emissive.set(0.0, 0.0, 0.0);

const spriteObject = new RC.Sprite({geometry: spriteGeometry, material: spriteMaterial});
spriteObject.position.set(0.0, 0.0, 0.0);
spriteObject.scale.set(0.1, 0.1, 0.1);
spriteObject.instancedTranslation = true;
spriteObject.instanceCount = spriteInstances.length;

scene.add(spriteObject);

GL resource management

A BufferAttribute or a Texture holds data. The WebGL buffer or texture built from it is only a cache of that data, owned by the GLManager of the context that built it. Nothing in RenderCore registers, reference-counts or disposes a GL resource: an object that stops being drawn simply stops being asked for, and the buffer behind it can be dropped and rebuilt from the data, which is still there.

Two consequences worth knowing.

The same attribute or texture can be used by several contexts at once. Each GLManager keeps its own cache entry -- the GL object, the version it last uploaded, its idle counter and the attrib locations it is bound to -- so two canvases sharing one geometry each upload it once, independently. Nothing that is per context lives on the attribute; the attribute carries only data and a version counter, bumped whenever the data changes.

Freeing memory is a question only the application can answer. The renderer sees a stream of draw calls and cannot tell an object that was removed from one that merely was not visible this frame. So the application brackets a change to the scene:

renderer.ageResources();      // before: age everything this context holds
// ... rebuild the scene: add, remove, replace ...
renderer.collectResources();  // after: drop whatever the rebuild did not use

"Still needed" is then answered by whether anything reached for the resource while the scene was being rebuilt -- no bookkeeping, no ownership, no destructors. collectResources(n) takes the number of cycles a resource may go untouched before it is dropped; the default of 2 gives one cycle of grace, so a resource used by every second rebuild is not thrashed. Dropping writes nothing back to the attribute or texture, so a resource collected too eagerly costs an upload and nothing else.

An application that loads a scene once and keeps it never has to call either one, which is why neither is on the render path. An application whose scene changes -- a new dataset, a new event -- calls them around each change and its GPU memory follows the scene on its own. ROOT's REve does exactly this, once per update cycle, in EveManager around the point where the scenes finish applying changes.

External sources

Framework contains some functionalities adopted from Three.js 3D library.

The list of such functionalities is as follows:

About

A lightweight deferred rendering WebGL 2.0 framework in JavaScript.

Resources

Stars

14 stars

Watchers

6 watching

Forks

Releases

Packages

Contributors

Languages