From 70226b4979a38abd7138d2e8c2de45a1e6eb188a Mon Sep 17 00:00:00 2001 From: ademola-lou <34609417+ademola-lou@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:16:17 +0000 Subject: [PATCH 1/2] chore: squash feature changes from feat-mesh-blend (exclude build files) --- examples/files.json | 7 +- examples/jsm/tsl/display/MeshBlendNode.js | 155 ++++++++++++++++ examples/tags.json | 1 + examples/webgpu_postprocessing_meshblend.html | 175 ++++++++++++++++++ test/e2e/puppeteer.js | 9 +- 5 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 examples/jsm/tsl/display/MeshBlendNode.js create mode 100644 examples/webgpu_postprocessing_meshblend.html diff --git a/examples/files.json b/examples/files.json index ff3787300cd25e..769295056255ab 100644 --- a/examples/files.json +++ b/examples/files.json @@ -160,9 +160,6 @@ "webgl_materials_video", "webgl_materials_video_webcam", "webgl_materials_wireframe", - "webgl_pmrem_cubemap", - "webgl_pmrem_equirectangular", - "webgl_pmrem_test", "webgl_math_obb", "webgl_math_orientation_transform", "webgl_mesh_batch", @@ -402,7 +399,6 @@ "webgpu_pmrem_cubemap", "webgpu_pmrem_equirectangular", "webgpu_pmrem_scene", - "webgpu_pmrem_test", "webgpu_portal", "webgpu_postprocessing_3dlut", "webgpu_postprocessing_afterimage", @@ -425,8 +421,8 @@ "webgpu_postprocessing_sobel", "webgpu_postprocessing_ssaa", "webgpu_postprocessing_ssgi", + "webgpu_postprocessing_meshblend", "webgpu_postprocessing_ssr", - "webgpu_postprocessing_sss", "webgpu_postprocessing_traa", "webgpu_postprocessing_transition", "webgpu_postprocessing", @@ -577,6 +573,7 @@ ], "tests": [ "webgl_furnace_test", + "webgl_pmrem_test", "misc_uv_tests" ] } diff --git a/examples/jsm/tsl/display/MeshBlendNode.js b/examples/jsm/tsl/display/MeshBlendNode.js new file mode 100644 index 00000000000000..0c7e340cec497f --- /dev/null +++ b/examples/jsm/tsl/display/MeshBlendNode.js @@ -0,0 +1,155 @@ +import { TempNode, MeshBasicNodeMaterial, RenderTarget, QuadMesh, Vector2 } from 'three/webgpu'; +import { nodeObject, vec4, vec3, float, modelPosition, modelWorldMatrix, Fn, NodeUpdateType, texture, screenUV, fract, vec2, dot, abs, sqrt, mix, saturate, If, Loop, int } from 'three/tsl'; + +//Source: https://www.jacktollenaar.top/mesh-seam-smoothing-blending#h.50wag6hqg9gh + +const _size = /*@__PURE__*/ new Vector2(); + +class MeshBlendNode extends TempNode { + + constructor( sceneOutputNode, sceneDepthNode, camera, scene ) { + + super( 'vec4' ); + this.sceneOutputNode = sceneOutputNode; + this.sceneDepthNode = sceneDepthNode; + this.updateBeforeType = NodeUpdateType.FRAME; + this.renderTarget = new RenderTarget( 1, 1 ); + this.mainCamera = camera; + this.mainScene = scene; + this.blendFactor = float( 1.2 ); + this.kernelSize = float( 5 ); + this.kernelRadius = float( 0.01 * this.blendFactor.value ); + this.depthFalloff = float( 0.001 * this.blendFactor.value ); + this.debugMaterial = new MeshBasicNodeMaterial(); + this._quadMesh = new QuadMesh( this.debugMaterial ); + + } + + setup() { + + const CustomHash = Fn( ( [ p ] ) => { + + var lp = fract( p.mul( 0.3183099 ).add( 0.1 ) ); + lp = lp.mul( 17.0 ); + return fract( lp.x.mul( lp.y ).mul( lp.z ).mul( lp.x.add( lp.y ).add( lp.z ) ) ); + + } ); + + this.hashShader = Fn( () => { + + const p = vec3( modelWorldMatrix.mul( vec3( modelPosition ) ) ).toVar(); + return vec4( CustomHash( p ), 0., 0., 1. ); + + } ); + this.hashMaterial = new MeshBasicNodeMaterial(); + this.hashMaterial.colorNode = this.hashShader(); + + const uv = screenUV; + const FinalOutputNode = Fn( ()=>{ + + // sampling helpers (capture outside Fn so they can be used with varying UV offsets) + const sampleRT = ( v ) => texture( this.renderTarget.textures[ 0 ], v ); + + const outputPassFunc1 = Fn( ( [ sceneDepthNode, uvNode, kernelSizeNode, kernelRadiusNode ] ) => { + + const sceneDepthVar = sceneDepthNode.toVar(); + + // kernelSizeNode is expected to be a numeric node with a .value available at build time + const kSize = kernelSizeNode.value || 0; + + const seamLocation = vec2( 0., 0. ).toVar(); + var minDist = float( 9999999. ).toVar(); + + const objectIDColor = sampleRT( uvNode ).toVar(); + + // Use TSL Loop so the iteration becomes shader-side loops + const k = int( kSize ); + Loop( { start: k.negate(), end: k, type: 'int', condition: '<=', name: 'x' }, ( { x } ) => { + + Loop( { start: k.negate(), end: k, type: 'int', condition: '<=', name: 'y' }, ( { y } ) => { + + const offset = vec2( x.toFloat(), y.toFloat() ).mul( kernelRadiusNode.mul( sceneDepthVar.r.mul( 0.3 ) ).div( float( kSize ) ) ).toVar(); + const SampleUV = uvNode.add( offset ).toVar(); + const sampledObjectIDColor = sampleRT( SampleUV ).toVar(); + If( sampledObjectIDColor.x.notEqual( objectIDColor.x ), () => { + + const dist = dot( offset, offset ); + If( dist.lessThan( minDist ), () => { + + minDist.assign( dist ); + seamLocation.assign( offset ); + + } ); + + } ); + + } ); + + } ); + + return vec4( seamLocation.x, seamLocation.y, minDist, 1. ); + + } ); + + const finalPass = Fn( ( [ sceneColor, mirroredColor, kernelRadiusNode, sceneDepth, otherDepth, depthFalloffNode, minDist ] ) => { + + const depthDiff = abs( otherDepth.r.sub( sceneDepth.r ) ); + + const maxSearchDistance = kernelRadiusNode.div( sceneDepth.r ); + const weight = saturate( float( 0.5 ).sub( sqrt( minDist ).div( maxSearchDistance ) ) ); + const depthWeight = saturate( float( 1. ).sub( depthDiff.div( depthFalloffNode.mul( kernelRadiusNode ) ) ) ); + const finalWeight = weight.mul( depthWeight ); + + return mix( sceneColor, mirroredColor, finalWeight ); + + } ); + + const pass1 = outputPassFunc1( + texture( this.sceneDepthNode, uv ), + uv, this.kernelSize, this.kernelRadius ); + + const mirroredColor = texture( this.sceneOutputNode, uv.add( pass1.xy.mul( 2. ) ) ); + const otherDepth = texture( this.sceneDepthNode, uv.add( pass1.xy.mul( 2. ) ) ); + + const sceneColor = texture( this.sceneOutputNode, uv ); + const sceneDepth = texture( this.sceneDepthNode, uv ); + return finalPass( sceneColor, mirroredColor, this.kernelRadius, sceneDepth, otherDepth, this.depthFalloff, pass1.z ); + + } )(); + return FinalOutputNode; + + } + + setSize( width, height ) { + + this.renderTarget.setSize( width, height ); + + } + + updateBefore( frame ) { + + const { renderer } = frame; + const size = renderer.getSize( _size ); + this.setSize( size.width, size.height ); + + this.mainScene.overrideMaterial = this.hashMaterial; + renderer.setRenderTarget( this.renderTarget ); + renderer.render( this.mainScene, this.mainCamera ); + + this.mainScene.overrideMaterial = null; + renderer.setRenderTarget( null ); + this._quadMesh.render( renderer ); + + } + + dispose() { + + this.renderTarget.dispose(); + this.hashMaterial.dispose(); + this.debugMaterial.dispose(); + + } + +} + +export const meshblend = ( sceneOutputNode, sceneDepthNode, camera, scene ) => nodeObject( new MeshBlendNode( sceneOutputNode, sceneDepthNode, camera, scene ) ); diff --git a/examples/tags.json b/examples/tags.json index eeffb34d4c0ffb..da185e991bc276 100644 --- a/examples/tags.json +++ b/examples/tags.json @@ -156,6 +156,7 @@ "webgpu_postprocessing_sobel": [ "filter", "edge detection" ], "webgpu_postprocessing_ssaa": [ "msaa", "multisampled" ], "webgpu_postprocessing_ssgi": [ "global illumination", "indirect diffuse" ], + "webgpu_postprocessing_meshblend": ["mesh blend"], "webgpu_refraction": [ "water" ], "webgpu_rtt": [ "renderTarget", "texture" ], "webgpu_rendertarget_2d-array_3d": [ "renderTarget", "2d-array", "3d" ], diff --git a/examples/webgpu_postprocessing_meshblend.html b/examples/webgpu_postprocessing_meshblend.html new file mode 100644 index 00000000000000..741ad6dcd2be2e --- /dev/null +++ b/examples/webgpu_postprocessing_meshblend.html @@ -0,0 +1,175 @@ + + + + three.js webgpu - MeshBlend + + + + + + +
+ + +
+ three.jsMeshBlend +
+ + Mesh Blend. Screen space effect to blend objects. +
+ + + + + + diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js index 42121748780212..6deea7972c7dc5 100644 --- a/test/e2e/puppeteer.js +++ b/test/e2e/puppeteer.js @@ -152,7 +152,6 @@ const exceptionList = [ 'webgpu_camera_logarithmicdepthbuffer', 'webgpu_lightprobe_cubecamera', 'webgpu_loader_materialx', - 'webgpu_materials_basic', 'webgpu_materials_video', 'webgpu_materialx_noise', 'webgpu_morphtargets_face', @@ -178,21 +177,15 @@ const exceptionList = [ 'webgpu_rendertarget_2d-array_3d', 'webgpu_materials_envmaps_bpcem', 'webgpu_postprocessing_ao', - 'webgpu_postprocessing_difference', - 'webgpu_postprocessing_dof', 'webgpu_postprocessing_sobel', 'webgpu_postprocessing_3dlut', 'webgpu_postprocessing_fxaa', 'webgpu_postprocessing_afterimage', 'webgpu_postprocessing_ca', 'webgpu_postprocessing_ssgi', - 'webgpu_postprocessing_sss', + 'webgpu_postprocessing_meshblend', 'webgpu_xr_native_layers', 'webgpu_volume_caustics', - 'webgpu_volume_lighting', - 'webgpu_volume_lighting_rectarea', - 'webgpu_reflection', - 'webgpu_ocean', // WebGPU idleTime and parseTime too low 'webgpu_compute_cloth', From 830586e645ae886ca579ed6dfa8367ee8129c07b Mon Sep 17 00:00:00 2001 From: ademola-lou <34609417+ademola-lou@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:23:36 +0000 Subject: [PATCH 2/2] Examples: webgpu_postprocessing_meshblend - remove unused imports and inspector nodes, tidy formatting - remove unused three/tsl imports (add, vec3, vec4, colorToDirection, sample, depth) - drop unused local variable (box) and unused MRT inspector nodes (diffuse, normal, velocity) - remove unused sceneNormal sample helper and related comment - minor formatting/spacing and string-quote cleanups for consistency --- examples/webgpu_postprocessing_meshblend.html | 54 ++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/examples/webgpu_postprocessing_meshblend.html b/examples/webgpu_postprocessing_meshblend.html index 741ad6dcd2be2e..79174ced37431d 100644 --- a/examples/webgpu_postprocessing_meshblend.html +++ b/examples/webgpu_postprocessing_meshblend.html @@ -32,14 +32,14 @@