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 PixelationNode #28802

Merged
merged 25 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b1275d8
sketched out draft of pixelation pass.
cmhhelgeson Jul 4, 2024
1da945a
Have normal and depth edges working
cmhhelgeson Jul 4, 2024
67c89e0
Pixel size modifier
cmhhelgeson Jul 4, 2024
dd03bf7
playing with render targets
cmhhelgeson Jul 6, 2024
43cb319
fix renderTarget issue
cmhhelgeson Jul 7, 2024
5648328
auto-mrt version of pixelation
cmhhelgeson Jul 10, 2024
57a011f
cleanup
cmhhelgeson Jul 10, 2024
e0d49da
remove any outside tests, logs, and changes
cmhhelgeson Jul 10, 2024
9269101
screenshot and cleanup
cmhhelgeson Jul 11, 2024
4db388a
more cleanup
cmhhelgeson Jul 11, 2024
036af7e
differentiate lighting from webgl version and modify to remove lighti…
cmhhelgeson Jul 11, 2024
550f063
final lighting adjustment
cmhhelgeson Jul 11, 2024
7788849
Revert lighting
cmhhelgeson Jul 11, 2024
63dfe76
bring back directionToColor
cmhhelgeson Jul 11, 2024
58cc81c
fix screenshot
cmhhelgeson Jul 11, 2024
c16e316
filtering fix
cmhhelgeson Jul 11, 2024
03f1135
fix normalView and add new screenshot
cmhhelgeson Jul 11, 2024
db46ead
normalzie uvNodeNormal
cmhhelgeson Jul 11, 2024
09893e9
remove unused directionToColor import, floor widtth and height of res…
cmhhelgeson Jul 11, 2024
2ea62ee
replace single expression tslFn function
cmhhelgeson Jul 11, 2024
cff1d4d
update lowerResolutionMaterial, remove unnecessary const color assign…
cmhhelgeson Jul 11, 2024
98e4c0e
revert to pixelationPass approach
cmhhelgeson Jul 15, 2024
99a9e23
Merge branch 'dev' into webgpu_pixelation_pass
cmhhelgeson Jul 15, 2024
8742026
fix lint issue, ignore puppeteer test for now
cmhhelgeson Jul 15, 2024
e338644
Update webgpu_postprocessing_pixel.html
Mugen87 Jul 15, 2024
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
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@
"webgpu_postprocessing_anamorphic",
"webgpu_postprocessing_ao",
"webgpu_postprocessing_dof",
"webgpu_postprocessing_pixel",
"webgpu_postprocessing_fxaa",
"webgpu_postprocessing_sobel",
"webgpu_postprocessing",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
277 changes: 277 additions & 0 deletions examples/webgpu_postprocessing_pixel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgpu - postprocessing pixel</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> - Node based pixelation pass with optional single pixel outlines by
<a href="https://github.com/KodyJKing" target="_blank" rel="noopener">Kody King</a><br /><br />
</div>

<div id="container"></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 { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

import { uniform, pixelationPass } from 'three/tsl';

let camera, scene, renderer, postProcessing, crystalMesh, clock;
let gui, effectController;

init();

function init() {

const aspectRatio = window.innerWidth / window.innerHeight;

camera = new THREE.OrthographicCamera( - aspectRatio, aspectRatio, 1, - 1, 0.1, 10 );
camera.position.y = 2 * Math.tan( Math.PI / 6 );
camera.position.z = 2;

scene = new THREE.Scene();
scene.background = new THREE.Color( 0x151729 );

clock = new THREE.Clock();

// textures

const loader = new THREE.TextureLoader();
const texChecker = pixelTexture( loader.load( 'textures/checker.png' ) );
const texChecker2 = pixelTexture( loader.load( 'textures/checker.png' ) );
texChecker.repeat.set( 3, 3 );
texChecker2.repeat.set( 1.5, 1.5 );

// meshes

const boxMaterial = new THREE.MeshPhongMaterial( { map: texChecker2 } );

function addBox( boxSideLength, x, z, rotation ) {

const mesh = new THREE.Mesh( new THREE.BoxGeometry( boxSideLength, boxSideLength, boxSideLength ), boxMaterial );
mesh.castShadow = true;
//mesh.receiveShadow = true;
mesh.rotation.y = rotation;
mesh.position.y = boxSideLength / 2;
mesh.position.set( x, boxSideLength / 2 + .0001, z );
scene.add( mesh );
return mesh;

}

addBox( .4, 0, 0, Math.PI / 4 );
addBox( .5, - .5, - .5, Math.PI / 4 );

const planeSideLength = 2;
const planeMesh = new THREE.Mesh(
new THREE.PlaneGeometry( planeSideLength, planeSideLength ),
new THREE.MeshPhongMaterial( { map: texChecker } )
);
planeMesh.receiveShadow = true;
planeMesh.rotation.x = - Math.PI / 2;
scene.add( planeMesh );

const radius = .2;
const geometry = new THREE.IcosahedronGeometry( radius );
crystalMesh = new THREE.Mesh(
geometry,
new THREE.MeshPhongMaterial( {
color: 0x68b7e9,
emissive: 0x4f7e8b,
shininess: 10,
specular: 0xffffff
} )
);
//crystalMesh.receiveShadow = true;
crystalMesh.castShadow = true;
scene.add( crystalMesh );

// lights

scene.add( new THREE.AmbientLight( 0x757f8e, 3 ) );

const directionalLight = new THREE.DirectionalLight( 0xfffecd, 1.5 );
directionalLight.position.set( 100, 100, 100 );
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.set( 2048, 2048 );
scene.add( directionalLight );

const spotLight = new THREE.SpotLight( 0xffc100, 10, 10, Math.PI / 16, .02, 2 );
spotLight.position.set( 2, 2, 0 );
const target = spotLight.target;
scene.add( target );
target.position.set( 0, 0, 0 );
spotLight.castShadow = true;
scene.add( spotLight );
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you mind creating the example with the exact same lighting conditions than the original version?

It's important to perform a 1:1 comparison so we can see possible deviations.

Copy link
Contributor Author

@cmhhelgeson cmhhelgeson Jul 11, 2024

Choose a reason for hiding this comment

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

I can apply the same lighting parameters to the scene, but the original lighting can't be replicated due to the shadow artifacts present, as mentioned in #28642. In my testing, recreating the lighting conditions of almost any webgl example that uses standard Three.js lights and casts shadows onto other objects exhibits similar rendering issues when that same sample is ported over to the WebGPURenderer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Then ignore the shadow casting for now. However, the type of lights and their parametrization should match otherwise the scene's color tone is different which makes it impossible to review the PR.


renderer = new THREE.WebGPURenderer( { antialias: false } );
renderer.shadowMap.enabled = true;
//renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
document.body.appendChild( renderer.domElement );

effectController = {
pixelSize: uniform( 6 ),
normalEdgeStrength: uniform( 0.3 ),
depthEdgeStrength: uniform( 0.4 ),
pixelAlignedPanning: true
};

postProcessing = new THREE.PostProcessing( renderer );
const scenePass = pixelationPass( scene, camera, effectController.pixelSize, effectController.normalEdgeStrength, effectController.depthEdgeStrength );
postProcessing.outputNode = scenePass;

window.addEventListener( 'resize', onWindowResize );

const controls = new OrbitControls( camera, renderer.domElement );
controls.maxZoom = 2;

// gui

gui = new GUI();
gui.add( effectController.pixelSize, 'value', 1, 16, 1 ).name( 'Pixel Size' );
gui.add( effectController.normalEdgeStrength, 'value', 0, 2, 0.05 ).name( 'Normal Edge Strength' );
gui.add( effectController.depthEdgeStrength, 'value', 0, 1, 0.05 ).name( 'Depth Edge Strength' );
gui.add( effectController, 'pixelAlignedPanning' );

}

function onWindowResize() {

const aspectRatio = window.innerWidth / window.innerHeight;
camera.left = - aspectRatio;
camera.right = aspectRatio;
camera.updateProjectionMatrix();

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

}

function animate() {

const t = clock.getElapsedTime();

crystalMesh.material.emissiveIntensity = Math.sin( t * 3 ) * .5 + .5;
crystalMesh.position.y = .7 + Math.sin( t * 2 ) * .05;
crystalMesh.rotation.y = stopGoEased( t, 2, 4 ) * 2 * Math.PI;

const rendererSize = renderer.getSize( new THREE.Vector2() );
const aspectRatio = rendererSize.x / rendererSize.y;

if ( effectController.pixelAlignedPanning ) {

const pixelSize = effectController.pixelSize.value;

pixelAlignFrustum( camera, aspectRatio, Math.floor( rendererSize.x / pixelSize ),
Math.floor( rendererSize.y / pixelSize ) );

} else if ( camera.left != - aspectRatio || camera.top != 1.0 ) {

// Reset the Camera Frustum if it has been modified
camera.left = - aspectRatio;
camera.right = aspectRatio;
camera.top = 1.0;
camera.bottom = - 1.0;
camera.updateProjectionMatrix();

}

postProcessing.render();

}

// Helper functions

function pixelTexture( texture ) {

texture.minFilter = THREE.NearestFilter;
texture.magFilter = THREE.NearestFilter;
texture.generateMipmaps = false;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.colorSpace = THREE.SRGBColorSpace;
return texture;

}

function easeInOutCubic( x ) {

return x ** 2 * 3 - x ** 3 * 2;

}

function linearStep( x, edge0, edge1 ) {

const w = edge1 - edge0;
const m = 1 / w;
const y0 = - m * edge0;
return THREE.MathUtils.clamp( y0 + m * x, 0, 1 );

}

function stopGoEased( x, downtime, period ) {

const cycle = ( x / period ) | 0;
const tween = x - cycle * period;
const linStep = easeInOutCubic( linearStep( tween, downtime, period ) );
return cycle + linStep;

}

function pixelAlignFrustum( camera, aspectRatio, pixelsPerScreenWidth, pixelsPerScreenHeight ) {
Fixed Show fixed Hide fixed

// 0. Get Pixel Grid Units
const worldScreenWidth = ( ( camera.right - camera.left ) / camera.zoom );
const worldScreenHeight = ( ( camera.top - camera.bottom ) / camera.zoom );
const pixelWidth = worldScreenWidth / pixelsPerScreenWidth;
const pixelHeight = worldScreenHeight / pixelsPerScreenHeight;

// 1. Project the current camera position along its local rotation bases
const camPos = new THREE.Vector3(); camera.getWorldPosition( camPos );
const camRot = new THREE.Quaternion(); camera.getWorldQuaternion( camRot );
const camRight = new THREE.Vector3( 1.0, 0.0, 0.0 ).applyQuaternion( camRot );
const camUp = new THREE.Vector3( 0.0, 1.0, 0.0 ).applyQuaternion( camRot );
const camPosRight = camPos.dot( camRight );
const camPosUp = camPos.dot( camUp );

// 2. Find how far along its position is along these bases in pixel units
const camPosRightPx = camPosRight / pixelWidth;
const camPosUpPx = camPosUp / pixelHeight;

// 3. Find the fractional pixel units and convert to world units
const fractX = camPosRightPx - Math.round( camPosRightPx );
const fractY = camPosUpPx - Math.round( camPosUpPx );

// 4. Add fractional world units to the left/right top/bottom to align with the pixel grid
camera.left = - aspectRatio - ( fractX * pixelWidth );
camera.right = aspectRatio - ( fractX * pixelWidth );
camera.top = 1.0 - ( fractY * pixelHeight );
camera.bottom = - 1.0 - ( fractY * pixelHeight );
camera.updateProjectionMatrix();

}

</script>
</body>

</html>
1 change: 1 addition & 0 deletions src/nodes/Nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export { default as Lut3DNode, lut3D } from './display/Lut3DNode.js';
export { default as GTAONode, ao } from './display/GTAONode.js';
export { default as FXAANode, fxaa } from './display/FXAANode.js';
export { default as RenderOutputNode, renderOutput } from './display/RenderOutputNode.js';
export { default as PixelationPassNode, pixelationPass } from './display/PixelationPassNode.js';

export { default as PassNode, pass, passTexture, depthPass } from './display/PassNode.js';

Expand Down
Loading