Skip to content

TSL: Force flat shading for geometries without normals for normal* nodes#32848

Merged
sunag merged 3 commits into
mrdoob:devfrom
sunag:dev-flat-shading
Jan 27, 2026
Merged

TSL: Force flat shading for geometries without normals for normal* nodes#32848
sunag merged 3 commits into
mrdoob:devfrom
sunag:dev-flat-shading

Conversation

@sunag

@sunag sunag commented Jan 24, 2026

Copy link
Copy Markdown
Collaborator

Related issue: #32831

Description

Force flat shading for geometries without normals for normal* nodes.

@github-actions

github-actions Bot commented Jan 24, 2026

Copy link
Copy Markdown

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 357.49
84.84
357.49
84.84
+0 B
+0 B
WebGPU 621.8
172.74
621.84
172.74
+37 B
-2 B
WebGPU Nodes 620.41
172.5
620.45
172.5
+37 B
-2 B

🌳 Bundle size after tree-shaking

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

Before After Diff
WebGL 490.27
119.86
490.27
119.86
+0 B
+0 B
WebGPU 692.71
187.98
692.75
187.98
+37 B
-2 B
WebGPU Nodes 642.09
175.08
642.13
175.08
+37 B
+2 B

@sunag sunag changed the title TSL: Force flat shading for geometries without normals for normals TSL: Force flat shading for geometries without normals for normal* nodes Jan 24, 2026
@sunag sunag marked this pull request as ready for review January 26, 2026 03:39
@sunag sunag added this to the r183 milestone Jan 26, 2026
@sunag sunag merged commit 8b4ee6e into mrdoob:dev Jan 27, 2026
10 checks passed
@sunag sunag deleted the dev-flat-shading branch January 27, 2026 01:23
@mrdoob

mrdoob commented Jan 27, 2026

Copy link
Copy Markdown
Owner

Thank you!

@mrdoob

mrdoob commented Feb 1, 2026

Copy link
Copy Markdown
Owner

I think this change broke webgpu_performance:

https://rawcdn.githack.com/mrdoob/three.js/dev/examples/webgpu_performance.html
https://threejs.org/examples/webgpu_performance.html

❯ Hmm, something broke the lighting in webgpu_performance since     
  367d779305281524009371b2d1ad541a1bcfd689. Can you invesigate what change  
  is the culprit?                                                           

⏺ Bash(git log --oneline 367d779305281524009371b2d1ad541a1bcfd689~5..HEAD)
  ⎿ 37b96bc46a Updated builds.
    ccd2e6da8d WebGPU_Display_StereoExample: Add new Anaglyph Techniques (#
    32905)
    … +48 lines (ctrl+o to expand)


⏺ Bash(git show 8b4ee6e869 --stat)
  ⎿  commit 8b4ee6e869be9eac63f2fdea643af85916399848
     Author: sunag <sunagbrasil@gmail.com>
     Date:   Mon Jan 26 22:23:21 2026 -0300
     … +10 lines (ctrl+o to expand)

⏺ Bash(git show 8b4ee6e869 --no-stat -p)
  ⎿  Error: Exit code 128
     fatal: unrecognized argument: --no-stat

⏺ Bash(git show 8b4ee6e869)
  ⎿  commit 8b4ee6e869be9eac63f2fdea643af85916399848
     Author: sunag <sunagbrasil@gmail.com>
     Date:   Mon Jan 26 22:23:21 2026 -0300
     … +195 lines (ctrl+o to expand)

⏺ Let me check the webgpu_performance example to understand what geometries it
  uses.

⏺ Searched for 2 patterns, read 2 files (ctrl+o to expand)

⏺ Let me look at how Fn passes arguments to understand if the refactor from
  destructuring to direct builder access might have issues.

⏺ Searched for 3 patterns, read 1 file (ctrl+o to expand)

⏺ Let me check if GLTFLoader computes normals for meshes that don't have them:

⏺ Searched for 2 patterns (ctrl+o to expand)

⏺ Let me also check the BindGroup change which might be related:

⏺ Bash(git show 6320341999 --stat)
  ⎿  commit 63203419997b8dd7348fac82c86582e54a3aa2e1
     Author: Michael Herzog <michael.herzog@human-interactive.org>
     Date:   Sat Jan 31 14:06:24 2026 +0100
     … +7 lines (ctrl+o to expand)

⏺ Bash(git show 6320341999)
  ⎿  commit 63203419997b8dd7348fac82c86582e54a3aa2e1
     Author: Michael Herzog <michael.herzog@human-interactive.org>
     Date:   Sat Jan 31 14:06:24 2026 +0100
     … +78 lines (ctrl+o to expand)

⏺ Let me check if builder.geometry might be null or undefined in some cases:

⏺ Searched for 1 pattern (ctrl+o to expand)

⏺ Let me check if there might be cases where geometry is null when
  isFlatShading() is called:

⏺ Searched for 1 pattern (ctrl+o to expand)

⏺ Let me clarify the commit order - the user said the issue started since commit
   367d779305:

⏺ Bash(git log --oneline 367d779305^..HEAD | tail -20)
  ⎿ 7f71a050c8 Editor: fix app publishing for project using WebGPURenderer
    (#32897)
    f6185d16f4 WebGPURenderer: Fix `overrideMaterial` node assignments. (#3
    … +25 lines (ctrl+o to expand)


⏺ Read 1 file (ctrl+o to expand)

⏺ Let me check if there's an issue when building environment/background nodes
  which might not have geometry:

⏺ Searched for 2 patterns, read 1 file (ctrl+o to expand)

⏺ Let me check how the builder's context property is set:

⏺ Searched for 1 pattern (ctrl+o to expand)

⏺ Let me check if there are cases where geometry could be null when
  isFlatShading() is called:

⏺ Read 1 file (ctrl+o to expand)

⏺ I found the issue. Looking at the code in Normal.js and the isFlatShading()
  method in NodeBuilder.js:

  // NodeBuilder.js:475-478
  isFlatShading() {
      return this.material.flatShading === true || this.geometry.hasAttribute(
  'normal' ) === false;
  }

  The problem is that this.geometry can be null (set at line 102: this.geometry 
  = ( object && object.geometry ) || null). When this.geometry is null, calling
  this.geometry.hasAttribute('normal') throws a TypeError.

  Let me verify this is the issue and propose a fix:

⏺ Read 1 file (ctrl+o to expand)

⏺ The bug is confirmed. The isFlatShading() method at NodeBuilder.js:477 will
  throw when this.geometry is null. Also, normalLocal in Normal.js:25 has the
  same issue: builder.geometry.hasAttribute( 'normal' ) will throw if geometry
  is null.

  Here's the fix:

⏺ Update(src/nodes/core/NodeBuilder.js)
  ⎿  Added 1 line, removed 1 line
      474     */
      475    isFlatShading() {
      476  
      477 -    return this.material.flatShading === true || this.geometry
          -.hasAttribute( 'normal' ) === false;                          
      477 +    return this.material.flatShading === true || ! this.geomet
          +ry || this.geometry.hasAttribute( 'normal' ) === false;       
      478  
      479    }
      480  

⏺ Update(src/nodes/accessors/Normal.js)
  ⎿  Added 1 line, removed 1 line
      22   */
      23  export const normalLocal = /*@__PURE__*/ ( Fn( ( builder ) => {
      24  
      25 -  if ( builder.geometry.hasAttribute( 'normal' ) === false ) { 
      25 +  if ( ! builder.geometry || builder.geometry.hasAttribute( 'no
         +rmal' ) === false ) {                                          
      26  
      27      warn( 'TSL: Vertex attribute "normal" not found on geometry
          .' );
      28  

⏺ Let me check for any other places with the same potential null geometry issue:

⏺ Searched for 1 pattern, read 3 files (ctrl+o to expand)

⏺ Update(src/nodes/accessors/Tangent.js)
  ⎿  Added 1 line, removed 1 line
      31  
      32    let node;
      33  
      34 -  if ( builder.subBuildFn === 'VERTEX' || builder.geometry.hasA
         -ttribute( 'tangent' ) ) {                                      
      34 +  if ( builder.subBuildFn === 'VERTEX' || ( builder.geometry &&
         + builder.geometry.hasAttribute( 'tangent' ) ) ) {              
      35  
      36      node = modelViewMatrix.mul( vec4( tangentLocal, 0 ) ).xyz.t
          oVarying( 'v_tangentView' ).normalize();
      37  

⏺ Update(src/nodes/accessors/Bitangent.js)
  ⎿  Added 1 line, removed 1 line
      53  
      54    let node;
      55  
      56 -  if ( builder.subBuildFn === 'VERTEX' || builder.geometry.hasA
         -ttribute( 'tangent' ) ) {                                      
      56 +  if ( builder.subBuildFn === 'VERTEX' || ( builder.geometry &&
         + builder.geometry.hasAttribute( 'tangent' ) ) ) {              
      57  
      58      node = getBitangent( normalView.cross( tangentView ), 'v_bi
          tangentView' ).normalize();
      59  

⏺ Update(src/nodes/functions/material/getGeometryRoughness.js)
  ⎿  Added 1 line, removed 1 line
      3  
      4  const getGeometryRoughness = /*@__PURE__*/ Fn( ( builder ) => {
      5  
      6 -  if ( builder.geometry.hasAttribute( 'normal' ) === false ) {  
      6 +  if ( ! builder.geometry || builder.geometry.hasAttribute( 'nor
        +mal' ) === false ) {                                            
      7  
      8      return float( 0 );
      9  

⏺ Now let me build to verify the fix:

⏺ Bash(npm run build-webgpu 2>&1 | tail -20)
  ⎿  npm error Missing script: "build-webgpu"
     npm error
     npm error To see a list of scripts, run:
     … +3 lines (ctrl+o to expand)

⏺ Bash(npm run 2>&1 | head -30)
  ⎿  Lifecycle scripts included in three@0.182.0:
       start
         npm run dev
     … +27 lines (ctrl+o to expand)

⏺ Bash(npm run build 2>&1)
  ⎿  > three@0.182.0 build
     > rollup -c utils/build/rollup.config.js
     … +21 lines (ctrl+o to expand)
  ⎿  (timeout 2m)

⏺ The build succeeded. Here's a summary of the issue and fix:

  Root Cause

  Commit 8b4ee6e869 ("TSL: Force flat shading for geometries without normals")
  introduced a bug where builder.geometry.hasAttribute() is called without
  checking if builder.geometry is null first.

  The NodeBuilder initializes geometry as:
  this.geometry = ( object && object.geometry ) || null;

  So this.geometry can be null when there's no object or the object has no
  geometry. When isFlatShading() or other functions tried to access
  this.geometry.hasAttribute(), it would throw a TypeError.

  Files Fixed

  1. src/nodes/core/NodeBuilder.js:477 — isFlatShading() method
  2. src/nodes/accessors/Normal.js:25 — normalLocal function
  3. src/nodes/accessors/Tangent.js:34 — tangentView function
  4. src/nodes/accessors/Bitangent.js:56 — bitangentView function
  5. src/nodes/functions/material/getGeometryRoughness.js:6 —
  getGeometryRoughness function

  The Fix

  Added null checks before accessing builder.geometry.hasAttribute():

  // Before
  builder.geometry.hasAttribute( 'normal' )

  // After  
  ! builder.geometry || builder.geometry.hasAttribute( 'normal' ) === false
  // or
  builder.geometry && builder.geometry.hasAttribute( 'tangent' )

  This ensures that when geometry is null, the code gracefully falls back to
  flat shading (using screen-space derivatives to compute normals) rather than
  throwing an error.

✻ Cooked for 5m 31s

@sunag

sunag commented Feb 1, 2026

Copy link
Copy Markdown
Collaborator Author

This appears to be related to #32906 👀

@mrdoob

mrdoob commented Feb 2, 2026

Copy link
Copy Markdown
Owner

Nice! Any ideas of why the E2E tests didn't catch this?

@sunag

sunag commented Feb 2, 2026

Copy link
Copy Markdown
Collaborator Author

Nice! Any ideas of why the E2E tests didn't catch this?

I think that's because the update only affected the WebGPUBackend.

@VegarRingdalAibel

Copy link
Copy Markdown
Contributor

@sunag

Maybe wrong place to ask..

Im getting this, when i use flatshading = true and without normals.

three.core.js:2001 THREE.TSL: Vertex attribute "normal" not found on geometry

Im not using normals, since I dont need them/want to save memory usage.
Is there any way to tell it, its not missing, but supposed to be like this..

@sunag

sunag commented May 8, 2026

Copy link
Copy Markdown
Collaborator Author

@VegarRingdalAibel Hmm... I thought we had solved this problem. I wasn’t able to reproduce it in webgpu_loader_gltf by applying flatShading, for example.

Could you provide a fiddle for the issue?

image

@VegarRingdalAibel

Copy link
Copy Markdown
Contributor

@sunag
Maybe its something with custom batchmesh I have
Just manually edited to three.webgpu.js to get it running now.

Ill try and find out it related since you dont have any issues.

image image

@VegarRingdalAibel

Copy link
Copy Markdown
Contributor

yes, its due to my custom batched mesh part, had to skip more part..

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.

3 participants