Skip to content

Commit

Permalink
Nodes: implement displacement map for materials (mrdoob#28246)
Browse files Browse the repository at this point in the history
* implement displacement map

* Convert PNG to 8bit per channel

* update screenshot

---------

Co-authored-by: aardgoose <angus.sawyer@email.com>
  • Loading branch information
aardgoose and aardgoose authored May 1, 2024
1 parent b8198a7 commit 32d19eb
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 1 deletion.
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 11 additions & 1 deletion examples/jsm/nodes/materials/NodeMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down
Binary file modified examples/models/obj/ninja/normal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
266 changes: 266 additions & 0 deletions examples/webgpu_materials_displacementmap.html
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions test/e2e/puppeteer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 32d19eb

Please sign in to comment.