Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -402,7 +399,6 @@
"webgpu_pmrem_cubemap",
"webgpu_pmrem_equirectangular",
"webgpu_pmrem_scene",
"webgpu_pmrem_test",
"webgpu_portal",
"webgpu_postprocessing_3dlut",
"webgpu_postprocessing_afterimage",
Expand All @@ -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",
Expand Down Expand Up @@ -577,6 +573,7 @@
],
"tests": [
"webgl_furnace_test",
"webgl_pmrem_test",
"misc_uv_tests"
]
}
155 changes: 155 additions & 0 deletions examples/jsm/tsl/display/MeshBlendNode.js
Original file line number Diff line number Diff line change
@@ -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 ) );
1 change: 1 addition & 0 deletions examples/tags.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" ],
Expand Down
167 changes: 167 additions & 0 deletions examples/webgpu_postprocessing_meshblend.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgpu - MeshBlend</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="example.css">
</head>
<body>

<div id="info">
<a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>

<div class="title-wrapper">
<a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>MeshBlend</span>
</div>

<small>Mesh Blend. Screen space effect to blend objects.</small>
</div>

<script type="importmap">
{
"imports": {
"three": "../build/three.webgpu.js",
"three/webgpu": "../build/three.webgpu.js",
"three/tsl": "../build/three.tsl.js",
"three/addons/": "./jsm/"
}
}
</script>

<script type="module">

import * as THREE from 'three/webgpu';
import { pass, mrt, output, normalView, diffuseColor, velocity, directionToColor } from 'three/tsl';
import { meshblend } from 'three/addons/tsl/display/MeshBlendNode.js';

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

import { Inspector } from 'three/addons/inspector/Inspector.js';

let camera, scene, renderer, postProcessing, controls, textureLoader, sphere1, sphere2, floor;

init();

async function init() {

camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.set( 0, 10, 30 );

scene = new THREE.Scene();
scene.background = new THREE.Color( '#33334C' );

renderer = new THREE.WebGPURenderer();
//renderer.setPixelRatio( window.devicePixelRatio ); // probably too costly for most hardware
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
renderer.shadowMap.enabled = true;
renderer.inspector = new Inspector();
document.body.appendChild( renderer.domElement );

//

controls = new OrbitControls( camera, renderer.domElement );
controls.target.set( 0, 7, 0 );
controls.enablePan = true;
controls.minDistance = 1;
controls.maxDistance = 100;
controls.update();

var directionalLight = new THREE.DirectionalLight( 0xffffff );
directionalLight.position.set( 1, 1, 1 );
scene.add( directionalLight );
//

postProcessing = new THREE.PostProcessing( renderer );

const scenePass = pass( scene, camera );
scenePass.setMRT( mrt( {
output: output,
diffuseColor: diffuseColor,
normal: directionToColor( normalView ),
velocity: velocity
} ) );

const scenePassColor = scenePass.getTextureNode( 'output' );
const scenePassDepth = scenePass.getTextureNode( 'depth' ).toInspector( 'Depth', () => {

return scenePass.getLinearDepthNode();

} );


const diffuseTexture = scenePass.getTexture( 'diffuseColor' );
diffuseTexture.type = THREE.UnsignedByteType;

const normalTexture = scenePass.getTexture( 'normal' );
normalTexture.type = THREE.UnsignedByteType;

// Ambient light
const ambientLight = new THREE.AmbientLight( 0x404040, 2 );
scene.add( ambientLight );

textureLoader = new THREE.TextureLoader();
const texture1 = textureLoader.load( 'textures/brick_diffuse.jpg' );
texture1.colorSpace = THREE.SRGBColorSpace;
const texture2 = textureLoader.load( 'textures/floors/FloorsCheckerboard_S_Diffuse.jpg' );
texture2.colorSpace = THREE.SRGBColorSpace;

sphere1 = new THREE.Mesh( new THREE.SphereGeometry( 3 ), new THREE.MeshPhongMaterial( { map: texture1 } ) );
sphere1.position.set( 5, 5, 0 );
scene.add( sphere1 );

sphere2 = new THREE.Mesh( new THREE.SphereGeometry( 3 ), new THREE.MeshPhongMaterial( { color: 'orange' } ) );
sphere2.position.set( 0, 5, 0 );
scene.add( sphere2 );

floor = new THREE.Mesh( new THREE.BoxGeometry( 50, 1, 50 ), new THREE.MeshPhongMaterial( { map: texture2 } ) );
scene.add( floor );
floor.position.set( 0, 2.5, 0 );

window.addEventListener( 'resize', onWindowResize );

const meshBlend = meshblend( scenePassColor, scenePassDepth, camera, scene );
postProcessing.outputNode = meshBlend;

const gui = renderer.inspector.createParameters( 'MeshBlend settings' );
gui.add( meshBlend.kernelSize, 'value', 1, 20 ).step( 1 ).name( 'kernel size' ).onChange( () => postProcessing.needsUpdate = true );
gui.add( meshBlend.blendFactor, 'value', 0.1, 10 ).step( 0.01 ).name( 'blend factor' ).onChange( () => {

meshBlend.kernelRadius.value = 0.01 * meshBlend.blendFactor.value;
meshBlend.depthFalloff.value = 0.0001 * meshBlend.blendFactor.value;
postProcessing.needsUpdate = true;

} );
// gui.add( meshBlend.kernelRadius, 'value', 0.01, 1 ).step( 0.01 ).name( 'kernel radius' ).onChange(() => postProcessing.needsUpdate = true);
// gui.add( meshBlend.depthFalloff, 'value', 0.001, 0.1 ).step( 0.01 ).name( 'depth falloff' ).onChange(() => postProcessing.needsUpdate = true);

postProcessing.needsUpdate = true;

}

function onWindowResize() {

const width = window.innerWidth;
const height = window.innerHeight;

camera.aspect = width / height;
camera.updateProjectionMatrix();

renderer.setSize( width, height );

}

function animate() {

controls.update();

sphere2.position.x = Math.sin( Date.now() * 0.001 ) * 3;
sphere1.position.y = 5 + Math.sin( Date.now() * 0.001 ) * 3;
postProcessing.render();

}

</script>
</body>
</html>
9 changes: 1 addition & 8 deletions test/e2e/puppeteer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
Loading