2
0
Эх сурвалжийг харах

Nodes: implement displacement map for materials (#28246)

* implement displacement map

* Convert PNG to 8bit per channel

* update screenshot

---------

Co-authored-by: aardgoose <[email protected]>
aardgoose 1 жил өмнө
parent
commit
32d19ebeef

+ 1 - 0
examples/files.json

@@ -342,6 +342,7 @@
 		"webgpu_loader_gltf_transmission",
 		"webgpu_loader_materialx",
 		"webgpu_materials",
+		"webgpu_materials_displacementmap",
 		"webgpu_materials_lightmap",
 		"webgpu_materials_sss",
 		"webgpu_materials_transmission",

+ 11 - 1
examples/jsm/nodes/materials/NodeMaterial.js

@@ -4,7 +4,7 @@ import { attribute } from '../core/AttributeNode.js';
 import { output, diffuseColor, varyingProperty } from '../core/PropertyNode.js';
 import { materialAlphaTest, materialColor, materialOpacity, materialEmissive, materialNormal } from '../accessors/MaterialNode.js';
 import { modelViewProjection } from '../accessors/ModelViewProjectionNode.js';
-import { transformedNormalView } from '../accessors/NormalNode.js';
+import { transformedNormalView, normalLocal } from '../accessors/NormalNode.js';
 import { instance } from '../accessors/InstanceNode.js';
 import { batch } from '../accessors/BatchNode.js';
 import { materialReference } from '../accessors/MaterialReferenceNode.js';
@@ -216,6 +216,16 @@ class NodeMaterial extends ShaderMaterial {
 
 		}
 
+		if ( this.displacementMap ) {
+
+			const displacementMap = materialReference( 'displacementMap', 'texture' );
+			const displacementScale = materialReference( 'displacementScale', 'float' );
+			const displacementBias = materialReference( 'displacementBias', 'float' );
+
+			positionLocal.addAssign( normalLocal.normalize().mul( ( displacementMap.x.mul( displacementScale ).add( displacementBias ) ) ) );
+
+		}
+
 		if ( object.isBatchedMesh ) {
 
 			batch( object ).append();

BIN
examples/models/obj/ninja/normal.png


BIN
examples/screenshots/webgpu_materials_displacementmap.jpg


+ 266 - 0
examples/webgpu_materials_displacementmap.html

@@ -0,0 +1,266 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - materials - displacement map</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="main.css">
+	</head>
+
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - (normal + ao + displacement + environment) map demo.<br />
+			ninja head from <a href="https://gpuopen.com/archive/gamescgi/amd-gpu-meshmapper/" target="_blank" rel="noopener">AMD GPU MeshMapper</a>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
+			import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
+
+			import { MeshStandardNodeMaterial } from 'three/nodes';
+
+			let stats;
+			let camera, scene, renderer, controls;
+
+			const settings = {
+				metalness: 1.0,
+				roughness: 0.4,
+				ambientIntensity: 0.2,
+				aoMapIntensity: 1.0,
+				envMapIntensity: 1.0,
+				displacementScale: 2.436143, // from original model
+				normalScale: 1.0
+			};
+
+			let mesh, material;
+
+			let pointLight, ambientLight;
+
+			const height = 500; // of camera frustum
+
+			let r = 0.0;
+
+			init();
+			initGui();
+
+			// Init gui
+			function initGui() {
+
+				const gui = new GUI();
+
+				gui.add( settings, 'metalness' ).min( 0 ).max( 1 ).onChange( function ( value ) {
+
+					material.metalness = value;
+
+				} );
+
+				gui.add( settings, 'roughness' ).min( 0 ).max( 1 ).onChange( function ( value ) {
+
+					material.roughness = value;
+
+				} );
+
+				gui.add( settings, 'aoMapIntensity' ).min( 0 ).max( 1 ).onChange( function ( value ) {
+
+					material.aoMapIntensity = value;
+
+				} );
+
+				gui.add( settings, 'ambientIntensity' ).min( 0 ).max( 1 ).onChange( function ( value ) {
+
+					ambientLight.intensity = value;
+
+				} );
+
+				gui.add( settings, 'envMapIntensity' ).min( 0 ).max( 3 ).onChange( function ( value ) {
+
+					material.envMapIntensity = value;
+
+				} );
+
+				gui.add( settings, 'displacementScale' ).min( 0 ).max( 3.0 ).onChange( function ( value ) {
+
+					material.displacementScale = value;
+
+				} );
+
+				gui.add( settings, 'normalScale' ).min( - 1 ).max( 1 ).onChange( function ( value ) {
+
+					material.normalScale.set( 1, - 1 ).multiplyScalar( value );
+
+				} );
+
+			}
+
+			function init() {
+
+				const container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				renderer = new WebGPURenderer();
+				renderer.setAnimationLoop( animate );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				container.appendChild( renderer.domElement );
+
+				//
+
+				scene = new THREE.Scene();
+
+				const aspect = window.innerWidth / window.innerHeight;
+				camera = new THREE.OrthographicCamera( - height * aspect, height * aspect, height, - height, 1, 10000 );
+				camera.position.z = 1500;
+				scene.add( camera );
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.enableZoom = false;
+				controls.enableDamping = true;
+
+				// lights
+
+				ambientLight = new THREE.AmbientLight( 0xffffff, settings.ambientIntensity );
+				scene.add( ambientLight );
+
+				pointLight = new THREE.PointLight( 0xff0000, 1.5, 0, 0 );
+				pointLight.position.z = 2500;
+				scene.add( pointLight );
+
+				const pointLight2 = new THREE.PointLight( 0xff6666, 3, 0, 0 );
+				camera.add( pointLight2 );
+
+				const pointLight3 = new THREE.PointLight( 0x0000ff, 1.5, 0, 0 );
+				pointLight3.position.x = - 1000;
+				pointLight3.position.z = 1000;
+				scene.add( pointLight3 );
+
+				// env map
+
+				const path = 'textures/cube/SwedishRoyalCastle/';
+				const format = '.jpg';
+				const urls = [
+					path + 'px' + format, path + 'nx' + format,
+					path + 'py' + format, path + 'ny' + format,
+					path + 'pz' + format, path + 'nz' + format
+				];
+
+				const reflectionCube = new THREE.CubeTextureLoader().load( urls );
+
+				// textures
+
+				const textureLoader = new THREE.TextureLoader();
+				const normalMap = textureLoader.load( 'models/obj/ninja/normal.png' );
+				const aoMap = textureLoader.load( 'models/obj/ninja/ao.jpg' );
+				const displacementMap = textureLoader.load( 'models/obj/ninja/displacement.jpg' );
+
+				// material
+
+				material = new MeshStandardNodeMaterial( {
+
+					color: 0xc1c1c1,
+					roughness: settings.roughness,
+					metalness: settings.metalness,
+
+					normalMap: normalMap,
+					normalScale: new THREE.Vector2( 1, - 1 ), // why does the normal map require negation in this case?
+
+					aoMap: aoMap,
+					aoMapIntensity: 1,
+
+					displacementMap: displacementMap,
+					displacementScale: settings.displacementScale,
+					displacementBias: - 0.428408, // from original model
+
+					envMap: reflectionCube,
+					envMapIntensity: settings.envMapIntensity,
+
+					side: THREE.DoubleSide
+
+				} );
+
+				//
+
+				const loader = new OBJLoader();
+				loader.load( 'models/obj/ninja/ninjaHead_Low.obj', function ( group ) {
+
+					const geometry = group.children[ 0 ].geometry;
+					geometry.center();
+
+					mesh = new THREE.Mesh( geometry, material );
+					mesh.scale.multiplyScalar( 25 );
+					scene.add( mesh );
+
+				} );
+
+				//
+
+				stats = new Stats();
+				container.appendChild( stats.dom );
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				const aspect = window.innerWidth / window.innerHeight;
+
+				camera.left = - height * aspect;
+				camera.right = height * aspect;
+				camera.top = height;
+				camera.bottom = - height;
+
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function animate() {
+
+				controls.update();
+
+				stats.begin();
+				render();
+				stats.end();
+
+			}
+
+			function render() {
+
+				pointLight.position.x = 2500 * Math.cos( r );
+				pointLight.position.z = 2500 * Math.sin( r );
+
+				r += 0.01;
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -126,6 +126,7 @@ const exceptionList = [
 	'webgpu_camera_logarithmicdepthbuffer',
 	'webgpu_clipping',
 	'webgpu_loader_materialx',
+	'webgpu_materials_displacementmap',
 	'webgpu_materials_video',
 	'webgpu_materialx_noise',
 	'webgpu_morphtargets_face',