Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please honor the code style of the project. I suggest you configure your editor such that it supports the ESLint config from the project.


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
175 changes: 175 additions & 0 deletions examples/webgpu_postprocessing_meshblend.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<!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, add, vec3, vec4, directionToColor, colorToDirection, sample, depth } 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, box, 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 scenePassDiffuse = scenePass.getTextureNode( 'diffuseColor' ).toInspector( 'Diffuse Color' );
const scenePassDepth = scenePass.getTextureNode( 'depth' ).toInspector( 'Depth', () => {

return scenePass.getLinearDepthNode();

} );

const scenePassNormal = scenePass.getTextureNode( 'normal' ).toInspector( 'Normal' );
const scenePassVelocity = scenePass.getTextureNode( 'velocity' ).toInspector( 'Velocity' );

// bandwidth optimization

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

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

const sceneNormal = sample( ( uv ) => {

return colorToDirection( scenePassNormal.sample( uv ) );

} );

// 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>
Loading
Loading