Skip to content

WebGPURenderer: Improve RenderObject grouping - WIP - #32243

Draft
sunag wants to merge 35 commits into
mrdoob:devfrom
sunag:dev-instances-renderobject
Draft

sunag wants to merge 35 commits into
mrdoob:devfrom
sunag:dev-instances-renderobject

Conversation

@sunag

@sunag sunag commented Nov 11, 2025 •

Copy link
Copy Markdown
Collaborator

Related issue: #30560 (comment), #30560 (comment), #26673 (comment)

Description

This is a WIP.

The basic idea is to group the RenderObject so that we can handle similar objects.

Today we have a lot of redundant calls between similar objects in the rendering call that could be resolved in just one Backend.draw().

  • Improve RenderObject grouping

    • Add a key related to the object to separate properties relevant to shader creation, such as object.morphTargetInfluences and object.count for filterObjects, RenderObjects.get and RenderObject.getChainArray().
    • Remove the term RenderObject.instances and leave only RenderObject.objects
    • Implement the dispose logic
  • Create shared buffer

    • Create an extended buffer class designed to couple multiple uniforms.
    • Integrate shared buffer initially for modelWorldMatrix and modelNormalMatrix.
      • Introduce objectIndex
      • Introduce mediumpModelWorldMatrix
      • Introduce mediumpModelNormalMatrix
    • The buffer node is not being shared properly when shared with other materials, causing the buffer to be updated more than once in the same render call. WebGPURenderer: Improve cache bindings from node uniforms #32244
    • buffer does not have an integrated updateRanges range, which can be a problem if used in conjunction with instances since InstanceNode does not distinguish it with an attribute. WebGPURenderer: Add update ranges for bindings #32248
      // Both WebGPU and WebGL backends have UBO max limited to 64kb. Matrix count number bigger than 1000 ( 16 * 4 * 1000 = 64kb ) will fallback to attribute.
      if ( count <= 1000 ) {
      instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', Math.max( count, 1 ) ).element( instanceIndex );
      } else {

@github-actions

github-actions Bot commented Nov 11, 2025 •

Copy link
Copy Markdown

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
Core 390.08
102.3
390.08
102.3
+0 B
+0 B
WebGL 372.73
87.14
372.73
87.14
+0 B
+0 B
WebGPU 429.27
115.67
430.86
116.17
+1.59 kB
+506 B
WebGPU Nodes 716.94
196.98
719.75
197.85
+2.81 kB
+869 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 517.46
124.27
517.46
124.27
+0 B
+0 B
WebGPU 769.54
206.2
773.07
207.28
+3.53 kB
+1.08 kB
WebGPU Nodes 715.39
192.75
718.92
193.77
+3.53 kB
+1.02 kB


import * as THREE from 'three/webgpu';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { texture, uv, userData, fog, rangeFogFactor, color, context } from 'three/tsl';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import context.
} );*/
document.body.appendChild( renderer.domElement );

const controls = new OrbitControls( camera, renderer.domElement );

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable controls.
for ( let i = 0, l = group.children.length; i < l; i ++ ) {

const sprite = group.children[ i ];
const scale = Math.sin( time + sprite.position.x * 0.01 ) * 0.3 + 1.0;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable scale.

import * as THREE from 'three/webgpu';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { texture, uv, userData, fog, rangeFogFactor, color, context } from 'three/tsl';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import context.
} );*/
document.body.appendChild( renderer.domElement );

const controls = new OrbitControls( camera, renderer.domElement );

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable controls.
for ( let i = 0, l = group.children.length; i < l; i ++ ) {

const sprite = group.children[ i ];
const scale = Math.sin( time + sprite.position.x * 0.01 ) * 0.3 + 1.0;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable scale.
Comment thread src/nodes/accessors/ModelNode.js Fixed
@sunag

sunag commented Dec 7, 2025 •

Copy link
Copy Markdown
Collaborator Author

I've included [two identical examples] using 10k sprites without instances, (1eb6d6d) for testing purposes, one using the current build and the other using the source code from the new code.

I'm excited about the results;
/cc @Mugen87 @mrdoob

New - 144~ FPS

image

Current - 30~ FPS

image

Comment thread src/nodes/accessors/ModelNode.js Fixed
@sunag sunag added this to the r183 milestone Dec 7, 2025
@sunag

sunag commented Dec 9, 2025

Copy link
Copy Markdown
Collaborator Author

Good memories when this example works, hahaha.

32 is the number of objects that are sharing the same uniform-group in the object context (worldMatrix and normalMatrix).

image

@sunag

sunag commented Jan 31, 2026

Copy link
Copy Markdown
Collaborator Author

There's a thing that's been blocking this PR in a way.

When we create a Material on a new object, it will create a shader, but if we modify the same material and use it on a new object, we'll have two different types of materials.

This characteristic was useful when we used shadow resource nodes in scene.overrideMaterial where we only modified parameters. After merger this, this characteristic should disappear, and we'll probably need to create some extra dummies of the materials for that purpose, but I think it shouldn't be expected anyway.

if ( overrideMaterial.isShadowPassMaterial ) {
const { colorNode, depthNode, positionNode } = this._getShadowNodes( material );
if ( this.shadowMap.type === VSMShadowMap ) {
overrideMaterial.side = ( material.shadowSide !== null ) ? material.shadowSide : material.side;
} else {
overrideMaterial.side = ( material.shadowSide !== null ) ? material.shadowSide : _shadowSide[ material.side ];
}
if ( colorNode !== null ) overrideMaterial.colorNode = colorNode;
if ( depthNode !== null ) overrideMaterial.depthNode = depthNode;
if ( positionNode !== null ) overrideMaterial.positionNode = positionNode;
}

Does anyone use it this way? If necessary, I can create an example to demonstrate this.

/cc @Mugen87 @RenaudRohlinger

@Mugen87

Mugen87 commented Jan 31, 2026

Copy link
Copy Markdown
Collaborator

I just know that the code snippet from your last post is required to get shadows work. If we can't do that anymore, we need a replacement in any event.

@mrdoob mrdoob modified the milestones: r183, r184 Feb 18, 2026
<script type="module">

import * as THREE from 'three/webgpu';
import { texture, uv, userData, fog, rangeFogFactor, color } from 'three/tsl';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused imports fog, rangeFogFactor, texture, userData, uv.

let camera, scene, renderer;

let map;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable map.

let map;

let group;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable group.

let group;

let imageWidth = 1, imageHeight = 1;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable imageWidth.

let group;

let imageWidth = 1, imageHeight = 1;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable imageHeight.
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

🖼️ E2E screenshot tests

❌ 17 example(s) failed (full artifacts).

Example Expected Actual Diff
webgpu_custom_fog_scattering
webgpu_depth_texture
webgpu_geometry_loft
webgpu_instance_uniform
webgpu_lensflares
webgpu_lightprobes_complex
webgpu_loader_materialx
webgpu_materials_arrays
webgpu_postprocessing_motion_blur
webgpu_postprocessing_outline

…and 7 more: webgpu_shadowmap, webgpu_shadowmap_opacity, webgpu_shadowmap_vsm, webgpu_sprites_2, webgpu_sprites_debug, webgpu_sprites_old, webgpu_xr_shadows

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants