Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nodes: Add CubeMapNode. #29073

Merged
merged 1 commit into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@
"webgpu_materials",
"webgpu_materials_basic",
"webgpu_materials_displacementmap",
"webgpu_materials_envmaps",
"webgpu_materials_lightmap",
"webgpu_materials_matcap",
"webgpu_materials_sss",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
156 changes: 156 additions & 0 deletions examples/webgpu_materials_envmaps.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgpu - materials - environment maps</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 environment mapping example<br/>
Equirectangular Map by <a href="http://www.flickr.com/photos/jonragnarsson/2294472375/" target="_blank" rel="noopener">J&oacute;n Ragnarsson</a>.
</div>

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


<script type="module">

import * as THREE from 'three';

import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

let controls, camera, scene, renderer;
let textureEquirec, textureCube;
let sphereMesh, sphereMaterial, params;

init();

function init() {

// CAMERAS

camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.set( 0, 0, 2.5 );

// SCENE

scene = new THREE.Scene();

// Textures

const loader = new THREE.CubeTextureLoader();
loader.setPath( 'textures/cube/Bridge2/' );

textureCube = loader.load( [ 'posx.jpg', 'negx.jpg', 'posy.jpg', 'negy.jpg', 'posz.jpg', 'negz.jpg' ] );

const textureLoader = new THREE.TextureLoader();

textureEquirec = textureLoader.load( 'textures/2294472375_24a3b8ef46_o.jpg' );
textureEquirec.mapping = THREE.EquirectangularReflectionMapping;
textureEquirec.colorSpace = THREE.SRGBColorSpace;

scene.background = textureCube;

//

const geometry = new THREE.IcosahedronGeometry( 1, 15 );
sphereMaterial = new THREE.MeshBasicMaterial( { envMap: textureCube } );
sphereMesh = new THREE.Mesh( geometry, sphereMaterial );
scene.add( sphereMesh );

//

renderer = new THREE.WebGPURenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
document.body.appendChild( renderer.domElement );

//

controls = new OrbitControls( camera, renderer.domElement );
controls.minDistance = 1.5;
controls.maxDistance = 6;

//

params = {
Cube: function () {

scene.background = textureCube;

sphereMaterial.envMap = textureCube;
sphereMaterial.needsUpdate = true;

},
Equirectangular: function () {

scene.background = textureEquirec;

sphereMaterial.envMap = textureEquirec;
sphereMaterial.needsUpdate = true;

},
Refraction: false
};

const gui = new GUI( { width: 300 } );
gui.add( params, 'Cube' );
gui.add( params, 'Equirectangular' );
gui.add( params, 'Refraction' ).onChange( function ( value ) {

if ( value ) {

textureEquirec.mapping = THREE.EquirectangularRefractionMapping;
textureCube.mapping = THREE.CubeRefractionMapping;

} else {

textureEquirec.mapping = THREE.EquirectangularReflectionMapping;
textureCube.mapping = THREE.CubeReflectionMapping;

}

sphereMaterial.needsUpdate = true;

} );
gui.open();

window.addEventListener( 'resize', onWindowResize );

}

function onWindowResize() {

camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

renderer.setSize( window.innerWidth, window.innerHeight );

}

//

function animate() {

camera.lookAt( scene.position );
renderer.render( scene, camera );

}

</script>

</body>
</html>
3 changes: 2 additions & 1 deletion src/nodes/lighting/BasicEnvironmentNode.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import LightingNode from './LightingNode.js';
import { addNodeClass } from '../core/Node.js';
import { cubeMapNode } from '../utils/CubeMapNode.js';

class BasicEnvironmentNode extends LightingNode {

Expand All @@ -15,7 +16,7 @@ class BasicEnvironmentNode extends LightingNode {

// environment property is used in the finish() method of BasicLightingModel

builder.context.environment = this.envNode;
builder.context.environment = cubeMapNode( this.envNode );

}

Expand Down
157 changes: 157 additions & 0 deletions src/nodes/utils/CubeMapNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import TempNode from '../core/TempNode.js';
import { addNodeClass } from '../core/Node.js';
import { NodeUpdateType } from '../core/constants.js';
import { nodeProxy } from '../shadernode/ShaderNode.js';
import { CubeTexture } from '../../textures/CubeTexture.js';
import { cubeTexture } from '../accessors/CubeTextureNode.js';
import CubeRenderTarget from '../../renderers/common/CubeRenderTarget.js';
import { CubeReflectionMapping, CubeRefractionMapping, EquirectangularReflectionMapping, EquirectangularRefractionMapping } from '../../constants';

const _cache = new WeakMap();

class CubeMapNode extends TempNode {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@sunag This module should be for internal use only so it is not exported in Nodes.js. Still, I'm a bit unsure about the name CubeMapNode since its quite similar to CubeTextureNode.

CubeMapNode should be in some sense the non-PBR version of PMREMNode. Meaning it encapsulate different inputs to provide a single environment map format for the material.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure if we need this, wouldn't just manipulating the UV be enough since we don't need to pre-filter for non-PBR?

import { texture, equirectUV, reflectVector, refractVector } from 'three/ts';

const coord = reflectVector; // refractVector 
const env = texture( textureEquirec, equirectUV( coord ) );
Copy link
Collaborator Author

@Mugen87 Mugen87 Aug 7, 2024

Choose a reason for hiding this comment

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

The problem with using equirectangular uvs are the artifacts that result at the poles which do not occur with the cube map format. They were previously reported as bugs which was one reason for not directly supporting equirectangular textures in the shader.

I'm afraid users will complain when the artifacts appear again when migrating from WebGLRenderer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Interesting, if so let's merge the PR, it seems good to me.


constructor( envNode ) {

super( 'vec3' );

this.envNode = envNode;

this._cubeTexture = null;
this._cubeTextureNode = cubeTexture();

const defaultTexture = new CubeTexture();
defaultTexture.isRenderTargetTexture = true;

this._defaultTexture = defaultTexture;

this.updateBeforeType = NodeUpdateType.RENDER;

}

updateBefore( frame ) {

const { renderer, material } = frame;

const envNode = this.envNode;

if ( envNode.isTextureNode || envNode.isMaterialReferenceNode ) {

const texture = ( envNode.isTextureNode ) ? envNode.value : material[ envNode.property ];

if ( texture && texture.isTexture ) {

const mapping = texture.mapping;

if ( mapping === EquirectangularReflectionMapping || mapping === EquirectangularRefractionMapping ) {

// check for converted cubemap map

if ( _cache.has( texture ) ) {

const cubeMap = _cache.get( texture );

mapTextureMapping( cubeMap, texture.mapping );
this._cubeTexture = cubeMap;

} else {

// create cube map from equirectangular map

const image = texture.image;

if ( isEquirectangularMapReady( image ) ) {

const renderTarget = new CubeRenderTarget( image.height );
renderTarget.fromEquirectangularTexture( renderer, texture );

mapTextureMapping( renderTarget.texture, texture.mapping );
this._cubeTexture = renderTarget.texture;

_cache.set( texture, renderTarget.texture );

texture.addEventListener( 'dispose', onTextureDispose );

} else {

// default cube texture as fallback when equirectangular texture is not yet loaded

this._cubeTexture = this._defaultTexture;

}

}

//

this._cubeTextureNode.value = this._cubeTexture;

} else {

// envNode already refers to a cube map

this._cubeTextureNode = this.envNode;

}

}

}

}

setup( builder ) {

this.updateBefore( builder );

return this._cubeTextureNode;

}

}

function isEquirectangularMapReady( image ) {

if ( image === null || image === undefined ) return false;

return image.height > 0;

}

function onTextureDispose( event ) {

const texture = event.target;

texture.removeEventListener( 'dispose', onTextureDispose );

const renderTarget = _cache.get( texture );

if ( renderTarget !== undefined ) {

_cache.delete( texture );

renderTarget.dispose();

}

}

function mapTextureMapping( texture, mapping ) {

if ( mapping === EquirectangularReflectionMapping ) {

texture.mapping = CubeReflectionMapping;

} else if ( mapping === EquirectangularRefractionMapping ) {

texture.mapping = CubeRefractionMapping;

}

}

export const cubeMapNode = nodeProxy( CubeMapNode );

addNodeClass( 'CubeMapNode', CubeMapNode );

export default CubeMapNode;