惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
O
OpenAI News
AI
AI
Cloudbric
Cloudbric
Security Archives - TechRepublic
Security Archives - TechRepublic
H
Heimdal Security Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
Threat Research - Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
腾讯CDC
S
SegmentFault 最新的问题
宝玉的分享
宝玉的分享
Cyberwarzone
Cyberwarzone
博客园 - 【当耐特】
TaoSecurity Blog
TaoSecurity Blog
The Cloudflare Blog
V
Visual Studio Blog
Forbes - Security
Forbes - Security
Apple Machine Learning Research
Apple Machine Learning Research
Spread Privacy
Spread Privacy
L
LINUX DO - 最新话题
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
K
Kaspersky official blog
IT之家
IT之家
大猫的无限游戏
大猫的无限游戏
The Last Watchdog
The Last Watchdog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园_首页
Cisco Talos Blog
Cisco Talos Blog
Security Latest
Security Latest
量子位
W
WeLiveSecurity
V
V2EX
L
Lohrmann on Cybersecurity
S
Security @ Cisco Blogs
小众软件
小众软件
The Hacker News
The Hacker News
阮一峰的网络日志
阮一峰的网络日志
H
Hacker News: Front Page
博客园 - 聂微东
T
Troy Hunt's Blog
S
Schneier on Security
有赞技术团队
有赞技术团队
爱范儿
爱范儿
博客园 - 三生石上(FineUI控件)
Webroot Blog
Webroot Blog
P
Privacy International News Feed
Jina AI
Jina AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

WebGL Fundamentals

WebGL Using 2 or More Textures WebGL Implementing DrawImage WebGL 2D Matrices WebGL Implementing A Matrix Stack WebGL 2D Rotation WebGL 2D Scale WebGL 2D Translation WebGL - Rasterization vs 3D libraries WebGL 3D - Cameras WebGL 3D Geometry - Lathe WebGL 3D - Directional Lighting WebGL 3D - Point Lighting WebGL 3D - Normal Mapping WebGL 3D - Spot Lighting WebGL - Orthographic 3D WebGL 3D Perspective Correct Texture Mapping WebGL 3D Perspective WebGL Textures WebGL and Alpha WebGL - Animation WebGL Anti-Patterns WebGL Attributes WebGL Boilerplate WebGL - Cross Origin Images WebGL Cross Platform Issues WebGL Cubemaps WebGL 3D - Data Textures WebGL - Drawing Multiple Things WebGL Drawing Without Data WebGL Environment Maps (reflections) WebGL Fog WebGL Framebuffers WebGL Fundamentals WebGL GPGPU WebGL How It Works WebGL Image Processing Continued WebGL Image Processing WebGL Indexed Vertices WebGL Optimization - Instanced Drawing WebGL - Less Code, More Fun WebGL Load Obj with Mtl WebGL Load Obj WebGL Matrices vs Math Matrices WebGL Multiple Views, Multiple Canvases WebGL Picking WebGL Planar and Perspective Projection Mapping WebGL Points, Lines, and Triangles WebGL Post Processing WebGL Precision Issues WebGL Pulling Vertices Accessing textures by pixel coordinate in WebGL2 A simple way to show the load on the GPU's vertex and fragment processing? Apply a displacement map and specular map Can anyone explain what this GLSL fragment shader is doing? Can I mute the warning about vertex attrib 0 being disabled? Create image warping effect in WebGL Creating a smudge/liquify effect How to draw Depth Sprites Determine min/max values for the entire image Don't blend a polygon that crosses itself Drawing 2D image with depth map to achieve pseudo-3D effect Drawing a heightmap Drawing layers with different points Drawing Many different models in a single draw call Drawing textured sprites with instanced drawing Efficient particle system in javascript? (WebGL) Emulating palette based graphics in WebGL FPS-like camera movement with basic matrix transformations Get the size of a point for collision checking GLSL shader to support coloring and texturing How can I compute for 500 points which of 1000 line segments is nearest to each point? How can I create a 16bit historgram of 16bit data How can I get all the uniforms and uniformBlocks How can I move the perspective vanishing point from the center of the canvas? How to Achieve Moving Line with Trail Effects How to bind an array of textures to a WebGL shader uniform? How to blend colors across 2 triangles How to combine more text drawing into fewer draw calls How to control the color between vertices How to create a torus How to detect clipped triangles in the framgment shader How to determine the average brightness in a scene? How to draw correctly textured trapezoid polygons How to fade the drawing buffer How to figure out how much GPU work to do without crashing WebGL How to get audio data into a shader How to get code completion for WebGL in Visual Studio Code How to get the 3d coordinates of a mouse click How to get pixelize effect in webgl? How to implement zoom from mouse in 2D WebGL How to import a heightmap in WebGL How to load images in the background with no jank How to make a smudge brush tool How to make WebGL canvas transparent How to optimize rendering a UI How to prevent texture bleeding with a texture atlas How to process particle positions How to read a single component with readPixels How to render large scale images like 32000x32000 How to simulate a 3D texture in WebGL
WebGL - Scene Graph
WebGLFundame · 2025-02-26 · via WebGL Fundamentals

This article is a continuation of previous WebGL articles. The previous article was about drawing multiple things. If you haven't read them I suggest you start there.

I'm sure some CS guru or graphics guru is going to give me an ear full but ... A scene graph is usually a tree structure where each node in the tree generates a matrix... hmmm, that's not a very useful definition. Maybe some examples would be useful.

Most 3D engines use a scene graph. You put the things you want to appear in the scene graph. The engine then walks the scene graph and figures out a list of things to draw. Scene graphs are hierarchical so for example if you wanted to make a universe simulation you might have a graph that looks like this

What's the point of a scene graph? The #1 feature of a scene graph is it provides a parent child relationship for matrices as we discussed in 2D matrix math. So for example in a simple (but unrealistic) universe simulation the stars (children), move along with their galaxy (parent). Similarly a moon (child) moves along with its planet (parent). If you move the earth the moon will move with it. If you move a galaxy all the stars inside will move with it. Drag the names in the diagram above and hopefully you can see their relationships.

If you go back to the 2D matrix math you might remember we multiply lots of matrices in order to translate, rotate, and scale objects. A scene graph provides a structure to help decide what matrix math to apply to an object.

Typically each Node in a scene graph represents a local space. Given the correct matrix math anything in that local space can ignore everything above it. Another way to state the same thing is that the moon only has to care about orbiting the earth. It does not have to care about orbiting the sun. Without this scene graph structure you'd have to do much more complex math to compute how to get the moon to orbit the sun because its orbit around the sun looks something like this

With a scene graph you just make the moon a child of the earth and then orbit the earth which is simple. The scene graph takes care of the fact that the earth is orbiting the sun. It does this by walking the nodes and multiplying the matrices as it walks

worldMatrix = greatGrandParent * grandParent * parent * self(localMatrix)

In concrete terms our universe simulation that would be

worldMatrixForMoon = galaxyMatrix * starMatrix * planetMatrix * moonMatrix;

We can do this very simply with a recursive function which is effectively

function computeWorldMatrix(currentNode, parentWorldMatrix) {
    // compute our world matrix by multiplying our local matrix with
    // our parent's world matrix.
    var worldMatrix = m4.multiply(parentWorldMatrix, currentNode.localMatrix);

    // now do the same for all of our children
    currentNode.children.forEach(function(child) {
        computeWorldMatrix(child, worldMatrix);
    });
}

This brings up some terminology which is pretty common to 3D scene graphs.

  • localMatrix: The local matrix for the current node. It transforms it and its children in local space with itself at the origin.

  • worldMatrix: For a given node it takes stuff in the local space of that node and transforms it to the space of the root node of the scene graph. Or in other words it places it in the world. If we compute the worldMatrix for the moon we'll get that funky orbit you see above.

A scene graph is pretty easy to make. Let's define a simple Node object. There's a zillion ways to organize a scene graph and I'm not sure which way is best. The most common is to have an optional field of thing to draw

var node = {
   localMatrix: ...,  // the "local" matrix for this node
   worldMatrix: ...,  // the "world" matrix for this node
   children: [],      // array of children
   thingToDraw: ??,   // thing to draw at this node
};

Let's make a solar system scene graph. I'm not going to use fancy textures or anything like that as it would clutter the example. First let's make a few functions to help manage nodes. First we'll make a node class

var Node = function() {
  this.children = [];
  this.localMatrix = m4.identity();
  this.worldMatrix = m4.identity();
};

Let's give it a way to set the parent of a node.

Node.prototype.setParent = function(parent) {
  // remove us from our parent
  if (this.parent) {
    var ndx = this.parent.children.indexOf(this);
    if (ndx >= 0) {
      this.parent.children.splice(ndx, 1);
    }
  }

  // Add us to our new parent
  if (parent) {
    parent.children.append(this);
  }
  this.parent = parent;
};

And here's the code to compute world matrices from local matrices based on their parent-child relationships. If we start at the parent and recursively visit the children we can compute their world matrices. If you don't understand matrix math check out this article on them.

Node.prototype.updateWorldMatrix = function(parentWorldMatrix) {
  if (parentWorldMatrix) {
    // a matrix was passed in so do the math and
    // store the result in `this.worldMatrix`.
    m4.multiply(this.localMatrix, parentWorldMatrix, this.worldMatrix);
  } else {
    // no matrix was passed in so just copy localMatrix to worldMatrix
    m4.copy(this.localMatrix, this.worldMatrix);
  }

  // now process all the children
  var worldMatrix = this.worldMatrix;
  this.children.forEach(function(child) {
    child.updateWorldMatrix(worldMatrix);
  });
};

Let's just do the sun, the earth, and the moon to keep it simple. We'll of course use fake distances so things fit on the screen. We'll just use a single sphere model and color it yellowish for the sun, blue-greenish for the earth and grayish for the moon. If drawInfo, bufferInfo, and programInfo are not familiar to you see the previous article.

// Let's make all the nodes
var sunNode = new Node();
sunNode.localMatrix = m4.translation(0, 0, 0);  // sun at the center
sunNode.drawInfo = {
  uniforms: {
    u_colorOffset: [0.6, 0.6, 0, 1], // yellow
    u_colorMult:   [0.4, 0.4, 0, 1],
  },
  programInfo: programInfo,
  bufferInfo: sphereBufferInfo,
};

var earthNode = new Node();
earthNode.localMatrix = m4.translation(100, 0, 0);  // earth 100 units from the sun
earthNode.drawInfo = {
  uniforms: {
    u_colorOffset: [0.2, 0.5, 0.8, 1],  // blue-green
    u_colorMult:   [0.8, 0.5, 0.2, 1],
  },
  programInfo: programInfo,
  bufferInfo: sphereBufferInfo,
};

var moonNode = new Node();
moonNode.localMatrix = m4.translation(20, 0, 0);  // moon 20 units from the earth
moonNode.drawInfo = {
  uniforms: {
    u_colorOffset: [0.6, 0.6, 0.6, 1],  // gray
    u_colorMult:   [0.1, 0.1, 0.1, 1],
  },
  programInfo: programInfo,
  bufferInfo: sphereBufferInfo,
};

Now that we've made the nodes let's connect them.

// connect the celestial objects
moonNode.setParent(earthNode);
earthNode.setParent(sunNode);

We'll again make a list of objects and a list of objects to draw.

var objects = [
  sunNode,
  earthNode,
  moonNode,
];

var objectsToDraw = [
  sunNode.drawInfo,
  earthNode.drawInfo,
  moonNode.drawInfo,
];

At render time we'll update each object's local matrix by rotating it slightly.

// update the local matrices for each object.
m4.multiply(m4.yRotation(0.01), sunNode.localMatrix  , sunNode.localMatrix);
m4.multiply(m4.yRotation(0.01), earthNode.localMatrix, earthNode.localMatrix);
m4.multiply(m4.yRotation(0.01), moonNode.localMatrix , moonNode.localMatrix);

Now that the local matrices are updated we'll update all the world matrices

sunNode.updateWorldMatrix();

Finally now that we have world matrices we need to multiply those to get a worldViewProjection matrix for each object.

// Compute all the matrices for rendering
objects.forEach(function(object) {
  object.drawInfo.uniforms.u_matrix = m4.multiply(viewProjectionMatrix, object.worldMatrix);
});

Rendering is the same loop we saw in our last article.

You'll notice all of the planets are the same size. Let's try to make the earth larger

// earth 100 units from the sun
earthNode.localMatrix = m4.translation(100, 0, 0);
// make the earth twice as large
earthNode.localMatrix = m4.scale(earthNode.localMatrix, 2, 2, 2);

Oops. The moon got larger too. To fix this we could manually shrink the moon. A better solution though is to add more nodes to our scene graph. Instead of just

  sun
   |
  earth
   |
  moon

We'll change it to

 solarSystem
   |    |
   |   sun
   |
 earthOrbit
   |    |
   |  earth
   |
  moonOrbit
      |
     moon

This will let the earth rotate around the solarSystem but we can separately rotate and scale the sun and it won't affect the earth. Similarly the earth can rotate separately from the moon. Let's make more nodes for solarSystem, earthOrbit and moonOrbit.

var solarSystemNode = new Node();
var earthOrbitNode = new Node();
earthOrbitNode.localMatrix = m4.translation(100, 0, 0);  // earth orbit 100 units from the sun
var moonOrbitNode = new Node();
moonOrbitNode.localMatrix = m4.translation(20, 0, 0);  // moon 20 units from the earth

Those orbit distances have been removed from the old nodes

var earthNode = new Node();
-// earth 100 units from the sun
-earthNode.localMatrix = m4.translation(100, 0, 0);
-// make the earth twice as large
-earthNode.localMatrix = m4.scale(earthNode.localMatrix, 2, 2, 2);
+earthNode.localMatrix = m4.scaling(2, 2, 2);   // make the earth twice as large

var moonNode = new Node();
-moonNode.localMatrix = m4.translation(20, 0, 0);  // moon 20 units from the earth

Connecting them now looks like this

// connect the celestial objects
sunNode.setParent(solarSystemNode);
earthOrbitNode.setParent(solarSystemNode);
earthNode.setParent(earthOrbitNode);
moonOrbitNode.setParent(earthOrbitNode);
moonNode.setParent(moonOrbitNode);

And we only need to update the orbits

// update the local matrices for each object.
-m4.multiply(m4.yRotation(0.01), sunNode.localMatrix  , sunNode.localMatrix);
-m4.multiply(m4.yRotation(0.01), earthNode.localMatrix, earthNode.localMatrix);
-m4.multiply(m4.yRotation(0.01), moonNode.localMatrix , moonNode.localMatrix);
+m4.multiply(m4.yRotation(0.01), earthOrbitNode.localMatrix, earthOrbitNode.localMatrix);
+m4.multiply(m4.yRotation(0.01), moonOrbitNode.localMatrix, moonOrbitNode.localMatrix);

// Update all world matrices in the scene graph
-sunNode.updateWorldMatrix();
+solarSystemNode.updateWorldMatrix();

And now you can see the earth is double size, the moon is not.

You might also notice the sun and the earth are no longer rotating in place. That is now independent.

Let's adjust a few more things.

-sunNode.localMatrix = m4.translation(0, 0, 0);  // sun at the center
+sunNode.localMatrix = m4.scaling(5, 5, 5);

...

+moonNode.localMatrix = m4.scaling(0.4, 0.4, 0.4);

...
// update the local matrices for each object.
matrixMultiply(earthOrbitNode.localMatrix, m4.yRotation(0.01), earthOrbitNode.localMatrix);
matrixMultiply(moonOrbitNode.localMatrix, m4.yRotation(0.01), moonOrbitNode.localMatrix);
+// spin the earth
+m4.multiply(m4.yRotation(0.05), earthNode.localMatrix, earthNode.localMatrix);
+// spin the moon
+m4.multiply(m4.yRotation(-0.01), moonNode.localMatrix, moonNode.localMatrix);

Currently we have a localMatrix and we're modifying it each frame. There's a problem though in that every frame our math will collect a little bit of error. There is a way to fix the math which is called ortho normalizing a matrix but even that won't always work. For example let's imagine we scaled down zero and back. Let's just do that for one value x

x = 246;       // frame #0, x = 246

scale = 1;
x = x * scale  // frame #1, x = 246

scale = 0.5;
x = x * scale  // frame #2, x = 123

scale = 0;
x = x * scale  // frame #3, x = 0

scale = 0.5;
x = x * scale  // frame #4, x = 0  OOPS!

scale = 1;
x = x * scale  // frame #5, x = 0  OOPS!

We lost our value. We can fix this by adding some other class that updates the matrix from other values. Let's change the Node definition to have a source. If it exists we'll ask the source to give us a local matrix.

*var Node = function(source) {
  this.children = [];
  this.localMatrix = m4.identity();
  this.worldMatrix = m4.identity();
+  this.source = source;
};

Node.prototype.updateWorldMatrix = function(matrix) {

+  var source = this.source;
+  if (source) {
+    source.getMatrix(this.localMatrix);
+  }

  ...

Now we can create a source. A common source is one that provides translation, rotation, and scale something like this

var TRS = function() {
  this.translation = [0, 0, 0];
  this.rotation = [0, 0, 0];
  this.scale = [1, 1, 1];
};

TRS.prototype.getMatrix = function(dst) {
  dst = dst || new Float32Array(16);
  var t = this.translation;
  var r = this.rotation;
  var s = this.scale;

  // compute a matrix from translation, rotation, and scale
  m4.translation(t[0], t[1], t[2], dst);
  matrixMultiply(m4.xRotation(r[0]), dst, dst);
  matrixMultiply(m4.yRotation(r[1]), dst, dst);
  matrixMultiply(m4.zRotation(r[2]), dst, dst);
  matrixMultiply(m4.scaling(s[0], s[1], s[2]), dst, dst);
  return dst;
};

And we can use it like this

// at init time making a node with a source
var someTRS  = new TRS();
var someNode = new Node(someTRS);

// at render time
someTRS.rotation[2] += elapsedTime;

Now there's no issue because we are recreating the matrix each time.

You might be thinking, I'm not making a solar system so what's the point? Well, If you wanted to animate a human you might have a scene graph that looks like this

How many joints you add for fingers and toes is up to you. The more joints you have the more power it takes to compute the animations and the more animation data it takes to provide info for all the joints. Old games like Virtua Fighter had around 15 joints. Games in the early to mid 2000s had 30 to 70 joints. If you did every joint in your hands there's at least 20 in each hand so just 2 hands is 40 joints. Many games that want to animate hands animate the thumb as one and the 4 fingers as one large finger to save on time (both CPU/GPU and artist time) and memory.

In any case here's a block guy I hacked together. It's using the TRS source for each node mentioned above. Programmer art and programmer animation FTW! :P

If you look at pretty much any 3D library you'll find a scene graph similar to this.

SetParent vs AddChild / RemoveChild

Many scene graphs have a node.addChild function and a node.removeChild function whereas above I made a node.setParent function. Which way is better is arguably a matter of style but I'd argue one objectively better reason setParent is better than addChild is because it makes code like this impossible.

    someParent.addChild(someNode);
    ...
    someOtherParent.addChild(someNode);

What does that mean? Does someNode get added to both someParent and someOtherParent? In most scene graphs that's impossible. Does the second call generate an error? ERROR: Already have parent. Does it magically remove someNode from someParent before adding it to someOtherParent? If it does it's certainly not clear from the name addChild.

setParent on the other hand has no such issue

    someNode.setParent(someParent);
    ...
    someNode.setParent(someOtherParent);

It's 100% obvious what's happening in this case. Zero ambiguity.