Skip to content

TSL Procedural Wood Example #31640

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: dev
Choose a base branch
from
Open

Conversation

SeeleyLogan
Copy link

Description

This pull requests extends the ThreeJS examples library and adds an addon to JSM.

The addon provides a material generator for procedural wood textures. The addon includes preset wood parameters that can easily be exposed and changed.

The example creates a scene demonstrating each wood preset imported from the addon.

The math behind the wood texture is ported from a blender tutorial by Lance Phan.

Creating procedural materials can be time consuming. This contribution will allow developers to skip lower-level wood material design; instead focusing on high level customization.

webgpu_procedural_wood

Caveat

Each wood texture has the same node graph; different parameters. This caused ThreeJS to cache the first shader and ignore the parameters. I avoided this issue by bypassing the caching altogether, causing my example to compile each material individually (regardless of its parameters). This shouldn't be necessary and I believe I am doing something wrong on that end.

This contribution is funded by DriveCore

@SeeleyLogan SeeleyLogan changed the title Tsl wood TSL Procedural Wood Example Aug 13, 2025
@bhouston
Copy link
Contributor

bhouston commented Aug 13, 2025

Nice work @SeeleyLogan. But yeah, the compile times are brutal. @sunag do you have an idea on how to make this performant given they are all basically the same material, just different parameters?

BTW what is really nice about this material is that it is fully 3D:

Screenshot 2025-08-13 at 12 13 49 PM

@Mugen87
Copy link
Collaborator

Mugen87 commented Aug 13, 2025

For testing: https://rawcdn.githack.com/SeeleyLogan/three.js/tsl_wood/examples/webgpu_tsl_wood.html

@sunag
Copy link
Collaborator

sunag commented Aug 13, 2025

@sunag do you have an idea on how to make this performant given they are all basically the same material, just different parameters?

My first suggestion is to use uniforms instead of constants for variable parameters, I didn't find any uniform in the code.

The code also needs to follow the mrdoob code style, I would also suggest creating an extended material from MeshPhysicalNodeMaterial instead of the GenerateWoodMaterial function.

I'm not sure if that would be enough, but I can continue the analysis as we progress.

@SeeleyLogan
Copy link
Author

@sunag I'm having trouble with these uniforms. The value of the uniforms need to be different per-material... If I use the same uniform it either updates all the materials or everything gets cached so it uses the first material.


export function GenerateWoodMaterial(params)
{
const material = new THREE.MeshPhysicalMaterial();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use MeshPhysicalNodeMaterial instead of MeshPhysicalMaterial


for (const key in params.wood_params) {
if (key === 'dark_wood_color' || key === 'light_wood_color') {
material.uniforms[key] = { value: new THREE.Color(params.wood_params[key]).convertLinearToSRGB() };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

material.uniforms[key] it would be an old approach, necessary to use the uniform node https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language#uniform

@sunag
Copy link
Collaborator

sunag commented Aug 13, 2025

@sunag I'm having trouble with these uniforms. The value of the uniforms need to be different per-material... If I use the same uniform it either updates all the materials or everything gets cached so it uses the first material.

If you use uniform for different materials and values, the final code will be the same, the node hierarchy too, so the system will not need to build and compile a new material/shader.

@cmhhelgeson
Copy link
Contributor

cmhhelgeson commented Aug 13, 2025

This is a great example, and a good test case for compilation too, lol.

Do the compilation issues relate to choosing .wgslFn over native TSL for certain functions? I don't see why this couldn't also work on the WebGL backend.

@sunag Is the WGSLEncoder robust enough where he could plop these functions into the decoder and get the correct TSL out for his WGSL functions?

@bhouston
Copy link
Contributor

@sunag I've updated the code to try to use uniforms. I think I did it correctly, but now all the woods are the same.

Screenshot 2025-08-13 at 4 34 12 PM

(I've also formatted the code and changed variables/method names to be more in line with MrDoob style.)

@WestLangley removed light and increased IBL intensity as well!

@SeeleyLogan
Copy link
Author

@cmhhelgeson I used wgslFn for functions I ported from Blender. It was easier to just change some minor syntax than to format each line.

Though I don't see a reason why regular TSL Fn wouldn't work.

@cmhhelgeson
Copy link
Contributor

cmhhelgeson commented Aug 13, 2025

@cmhhelgeson I used wgslFn for functions I ported from Blender. It was easier to just change some minor syntax than to format each line.

Though I don't see a reason why regular TSL Fn wouldn't work.

For the sake of completeness and aligning with the rest of the examples, I think it would be best to port these over to TSL. That way the program can work with devices that aren't compatible with WebGPU or don't expose WebGPU functionality (though those are growing fewer by the year). Technically, it also would allow your code to run on whatever graphics backends arrive to the web in the future (WebGPU2, MetalWeb, DoobGPU, or any other hypothetical paradigm that would be prevalent enough to integrate into Threejs).

If your initial source was GLSL, then you can port the GLSL code from Blender to TSL with the GLSLEncoder. The Threejs website provides an example that does this automatically: https://threejs.org/examples/?q=tsl#webgpu_tsl_transpiler.

@bhouston
Copy link
Contributor

bhouston commented Aug 13, 2025

@sunag we fixed the pieces all looking the same by using MeshPhysicalNodeMaterial as you suggested. Yeah!

Screenshot 2025-08-13 at 9 15 09 PM

Still pretty long loading time, but it is working now!

CC: @SeeleyLogan

@Mugen87
Copy link
Collaborator

Mugen87 commented Aug 14, 2025

When looking closely there are flat circle artifacts all over the materials. On a 5k display with a DPR of 2, these artifacts are quite prone to aliasing. So when moving the camera the materials produce a noisy and unstable result.

image

Besides, the grain pattern looks very aliased when zooming out.

Is this an expected side effect of the used technique?

@bhouston
Copy link
Contributor

@Mugen87

The aliasing is an issue. We likely have to figure out how to do analytical AA in the shader. But I figured we could contribute this back and then worry about it?

The flat circles are actually the cellular structure of the wood, or attempting to be. Here are pictures of real wood from my garage - notice the streaks from the cellular structure - this is maple and walnut, in other woods, they are not quite as prominent:

Screenshot 2025-08-14 at 10 31 34 AM Screenshot 2025-08-14 at 10 31 42 AM

@bhouston
Copy link
Contributor

Redid the scene and added incremental loading:

output

@Mugen87
Copy link
Collaborator

Mugen87 commented Aug 14, 2025

The aliasing is an issue. We likely have to figure out how to do analytical AA in the shader. But I figured we could contribute this back and then worry about it?

Yes, this isn't a blocker for the PR.

If we see shader aliasing here, could our TRAA save the day?

@SeeleyLogan
Copy link
Author

@Mugen87 It looks like I've fixed the aliasing issue. As the camera moves away, the sharp wood rings get blurred and the cell structure becomes smaller. There are a couple distances where the anti-aliasing breaks down slightly; nothing too noticeable.

@Mugen87
Copy link
Collaborator

Mugen87 commented Aug 14, 2025

Wow, much better!

@Mugen87 Mugen87 added this to the r180 milestone Aug 14, 2025
@Mugen87
Copy link
Collaborator

Mugen87 commented Aug 14, 2025

If the E2E screenshot makes troubles, I can try to regenerate it after the merge.

@sunag
Copy link
Collaborator

sunag commented Aug 14, 2025

I cached the wood function and added update events to the uniforms. The difference in performance was quite noticeable.

@cmhhelgeson suggested using TSL functions instead of WGSL functions, which I also think is positive.

The other point would be to create a WoodMaterial extended material instead of a function, I'm already assigning the parameters to the materials in the function cache I made. So halfway there is already done.

Don't take this as a block if you want to do this in later PRs.

@cmhhelgeson
Copy link
Contributor

@sunag The onObjectUpdate just creates a callback that updates the uniform whenever parameters on the material itself are changed, yes?

@hybridherbst
Copy link
Contributor

I think incremental loading just covers the fact that compilation is slow, I'd opt for removing it and loading everything at the same time (as a typical application would do).

We're also seeing very slow compile times with TSL (regular MeshPhysicalMaterial is ~50-100x slower to create and compile on WebGPURenderer vs. WebGLRenderer), so I think instead of covering it up this should stay as-is and compilation times hopefully improved at some point in the underlaying system.

@SeeleyLogan
Copy link
Author

@cmhhelgeson I've converted all instances of wgslFn to proper Fn.

@SeeleyLogan
Copy link
Author

@sunag

The other point would be to create a WoodMaterial extended material instead of a function, I'm already assigning the parameters to the materials in the function cache I made. So halfway there is already done.

Can you clarify what you mean by "WoodMaterial extended material instead of a function"?

@cmhhelgeson
Copy link
Contributor

Can you clarify what you mean by "WoodMaterial extended material instead of a function"?

ProceduralWood should be a class that extends MeshPhysicalNodeMaterial, rather than a function that creates, modifies, and returns an instance of MeshPhysicalNodeMaterial.

@bhouston
Copy link
Contributor

@hybridherbst wrote:

I think incremental loading just covers the fact that compilation is slow, I'd opt for removing it and loading everything at the same time (as a typical application would do).

We're also seeing very slow compile times with TSL (regular MeshPhysicalMaterial is ~50-100x slower to create and compile on WebGPURenderer vs. WebGLRenderer), so I think instead of covering it up this should stay as-is and compilation times hopefully improved at some point in the underlaying system.

We are of course seeing that as well with this wood texture. It is incredibly slow on mobile devices. So much that I am scared to deploy it to production use.

Should I create a Github issue for that? We could use this example as a test case.

SeeleyLogan and others added 25 commits August 22, 2025 21:02
(cherry picked from commit d1813a2)
(cherry picked from commit 2a35725)
(cherry picked from commit 9ae3876)
(cherry picked from commit 6fd32bb)
(cherry picked from commit eb5a62f)
(cherry picked from commit bbd22da)
(cherry picked from commit 7236e5e)
(cherry picked from commit 531d75c)
(cherry picked from commit dc8562d)
(cherry picked from commit fdc290a)
(cherry picked from commit e01ab4c)
Wood rings and cell structure suffered from aliasing.

Improved by blurring rings based on camera distance and changing cell size based on camera distance.

(cherry picked from commit b49b44b)
(cherry picked from commit 4691579)
Fix code style.

(cherry picked from commit bbbca28)
(cherry picked from commit 640fca8)
(cherry picked from commit 37abf5d)
Extended StandardMeshNodeMaterial to create WoodNodeMaterial

(cherry picked from commit 26f7487)
Converting voronoi3d to Fn caused frame drop and input lag. Merging e9f7c8b had no effect.

(cherry picked from commit 72057d2)
(cherry picked from commit 895ca13)
@bhouston
Copy link
Contributor

@mrdoob, I've fixed the PR to only have the key files included (cherrying picked all the important commits and force-pushed), refactored the example to make it cleaner and also demonstrate how to create custom wood materials, and not limit it to only presets.

CC: @SeeleyLogan

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.

8 participants