浏览代码

Examples: Add new pixelization effect. (#24873)

* Examples: Add new pixelization effect.

* Examples: Clean up.
Michael Herzog 2 年之前
父节点
当前提交
9cc9d75021

+ 0 - 48
examples/js/shaders/PixelShader.js

@@ -1,48 +0,0 @@
-( function () {
-
-	/**
- * Pixelation shader
- */
-
-	const PixelShader = {
-		uniforms: {
-			'tDiffuse': {
-				value: null
-			},
-			'resolution': {
-				value: null
-			},
-			'pixelSize': {
-				value: 1
-			}
-		},
-		vertexShader: /* glsl */`
-
-		varying highp vec2 vUv;
-
-			void main() {
-
-				vUv = uv;
-				gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
-
-		}`,
-		fragmentShader: /* glsl */`
-
-		uniform sampler2D tDiffuse;
-		uniform float pixelSize;
-		uniform vec2 resolution;
-
-		varying highp vec2 vUv;
-
-		void main(){
-
-			vec2 dxy = pixelSize / resolution;
-			vec2 coord = dxy * floor( vUv / dxy );
-			gl_FragColor = texture2D(tDiffuse, coord);
-
-		}`
-	};
-
-	THREE.PixelShader = PixelShader;
-
-} )();

+ 236 - 0
examples/jsm/postprocessing/RenderPixelatedPass.js

@@ -0,0 +1,236 @@
+import {
+	WebGLRenderTarget,
+	MeshNormalMaterial,
+	ShaderMaterial,
+	Vector2,
+	Vector4,
+	DepthTexture,
+	NearestFilter
+} from 'three';
+import { Pass, FullScreenQuad } from './Pass.js';
+
+class RenderPixelatedPass extends Pass {
+
+	constructor( pixelSize, scene, camera, options = {} ) {
+
+		super();
+
+		this.pixelSize = pixelSize;
+		this.resolution = new Vector2();
+		this.renderResolution = new Vector2();
+
+		this.pixelatedMaterial = this.createPixelatedMaterial();
+		this.normalMaterial = new MeshNormalMaterial();
+
+		this.fsQuad = new FullScreenQuad( this.pixelatedMaterial );
+		this.scene = scene;
+		this.camera = camera;
+
+		this.normalEdgeStrength = options.normalEdgeStrength || 0.3;
+		this.depthEdgeStrength = options.depthEdgeStrength || 0.4;
+
+		this.beautyRenderTarget = new WebGLRenderTarget();
+		this.beautyRenderTarget.texture.minFilter = NearestFilter;
+		this.beautyRenderTarget.texture.magFilter = NearestFilter;
+		this.beautyRenderTarget.depthTexture = new DepthTexture();
+
+		this.normalRenderTarget = new WebGLRenderTarget();
+		this.normalRenderTarget.texture.minFilter = NearestFilter;
+		this.normalRenderTarget.texture.magFilter = NearestFilter;
+
+
+
+	}
+
+	dispose() {
+
+		this.beautyRenderTarget.dispose();
+		this.normalRenderTarget.dispose();
+
+		this.pixelatedMaterial.dispose();
+		this.normalMaterial.dispose();
+
+		this.fsQuad.dispose();
+
+	}
+
+	setSize( width, height ) {
+
+		this.resolution.set( width, height );
+		this.renderResolution.set( ( width / this.pixelSize ) | 0, ( height / this.pixelSize ) | 0 );
+		const { x, y } = this.renderResolution;
+		this.beautyRenderTarget.setSize( x, y );
+		this.normalRenderTarget.setSize( x, y );
+		this.fsQuad.material.uniforms.resolution.value.set( x, y, 1 / x, 1 / y );
+
+	}
+
+	setPixelSize( pixelSize ) {
+
+		this.pixelSize = pixelSize;
+		this.setSize( this.resolution.x, this.resolution.y );
+
+	}
+
+	render( renderer, writeBuffer ) {
+
+		const uniforms = this.fsQuad.material.uniforms;
+		uniforms.normalEdgeStrength.value = this.normalEdgeStrength;
+		uniforms.depthEdgeStrength.value = this.depthEdgeStrength;
+
+		renderer.setRenderTarget( this.beautyRenderTarget );
+		renderer.render( this.scene, this.camera );
+
+		const overrideMaterial_old = this.scene.overrideMaterial;
+		renderer.setRenderTarget( this.normalRenderTarget );
+		this.scene.overrideMaterial = this.normalMaterial;
+		renderer.render( this.scene, this.camera );
+		this.scene.overrideMaterial = overrideMaterial_old;
+
+		uniforms.tDiffuse.value = this.beautyRenderTarget.texture;
+		uniforms.tDepth.value = this.beautyRenderTarget.depthTexture;
+		uniforms.tNormal.value = this.normalRenderTarget.texture;
+
+		if ( this.renderToScreen ) {
+
+			renderer.setRenderTarget( null );
+
+		} else {
+
+			renderer.setRenderTarget( writeBuffer );
+
+			if ( this.clear ) renderer.clear();
+
+		}
+
+		this.fsQuad.render( renderer );
+
+	}
+
+	createPixelatedMaterial() {
+
+		return new ShaderMaterial( {
+			uniforms: {
+				tDiffuse: { value: null },
+				tDepth: { value: null },
+				tNormal: { value: null },
+				resolution: {
+					value: new Vector4(
+						this.renderResolution.x,
+						this.renderResolution.y,
+						1 / this.renderResolution.x,
+						1 / this.renderResolution.y,
+					)
+				},
+				normalEdgeStrength: { value: 0 },
+				depthEdgeStrength: { value: 0 }
+			},
+			vertexShader:
+				`
+				varying vec2 vUv;
+
+				void main() {
+
+					vUv = uv;
+					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+
+				}
+				`,
+			fragmentShader:
+				`
+				uniform sampler2D tDiffuse;
+				uniform sampler2D tDepth;
+				uniform sampler2D tNormal;
+				uniform vec4 resolution;
+				uniform float normalEdgeStrength;
+				uniform float depthEdgeStrength;
+				varying vec2 vUv;
+
+				float getDepth(int x, int y) {
+
+					return texture2D( tDepth, vUv + vec2(x, y) * resolution.zw ).r;
+
+				}
+
+				vec3 getNormal(int x, int y) {
+
+					return texture2D( tNormal, vUv + vec2(x, y) * resolution.zw ).rgb * 2.0 - 1.0;
+
+				}
+
+				float depthEdgeIndicator(float depth, vec3 normal) {
+
+					float diff = 0.0;
+					diff += clamp(getDepth(1, 0) - depth, 0.0, 1.0);
+					diff += clamp(getDepth(-1, 0) - depth, 0.0, 1.0);
+					diff += clamp(getDepth(0, 1) - depth, 0.0, 1.0);
+					diff += clamp(getDepth(0, -1) - depth, 0.0, 1.0);
+					return floor(smoothstep(0.01, 0.02, diff) * 2.) / 2.;
+
+				}
+
+				float neighborNormalEdgeIndicator(int x, int y, float depth, vec3 normal) {
+
+					float depthDiff = getDepth(x, y) - depth;
+					vec3 neighborNormal = getNormal(x, y);
+					
+					// Edge pixels should yield to faces who's normals are closer to the bias normal.
+					vec3 normalEdgeBias = vec3(1., 1., 1.); // This should probably be a parameter.
+					float normalDiff = dot(normal - neighborNormal, normalEdgeBias);
+					float normalIndicator = clamp(smoothstep(-.01, .01, normalDiff), 0.0, 1.0);
+					
+					// Only the shallower pixel should detect the normal edge.
+					float depthIndicator = clamp(sign(depthDiff * .25 + .0025), 0.0, 1.0);
+
+					return (1.0 - dot(normal, neighborNormal)) * depthIndicator * normalIndicator;
+
+				}
+
+				float normalEdgeIndicator(float depth, vec3 normal) {
+					
+					float indicator = 0.0;
+
+					indicator += neighborNormalEdgeIndicator(0, -1, depth, normal);
+					indicator += neighborNormalEdgeIndicator(0, 1, depth, normal);
+					indicator += neighborNormalEdgeIndicator(-1, 0, depth, normal);
+					indicator += neighborNormalEdgeIndicator(1, 0, depth, normal);
+
+					return step(0.1, indicator);
+
+				}
+
+				void main() {
+
+					vec4 texel = texture2D( tDiffuse, vUv );
+
+					float depth = 0.0;
+					vec3 normal = vec3(0.0);
+
+					if (depthEdgeStrength > 0.0 || normalEdgeStrength > 0.0) {
+
+						depth = getDepth(0, 0);
+						normal = getNormal(0, 0);
+
+					}
+
+					float dei = 0.0;
+					if (depthEdgeStrength > 0.0) 
+						dei = depthEdgeIndicator(depth, normal);
+
+					float nei = 0.0; 
+					if (normalEdgeStrength > 0.0) 
+						nei = normalEdgeIndicator(depth, normal);
+
+					float Strength = dei > 0.0 ? (1.0 - depthEdgeStrength * dei) : (1.0 + normalEdgeStrength * nei);
+
+					gl_FragColor = texel * Strength;
+
+				}
+				`
+		} );
+
+	}
+
+}
+
+export { RenderPixelatedPass };

+ 0 - 44
examples/jsm/shaders/PixelShader.js

@@ -1,44 +0,0 @@
-/**
- * Pixelation shader
- */
-
-const PixelShader = {
-
-	uniforms: {
-
-		'tDiffuse': { value: null },
-		'resolution': { value: null },
-		'pixelSize': { value: 1 },
-
-	},
-
-	vertexShader: /* glsl */`
-
-		varying highp vec2 vUv;
-
-			void main() {
-
-				vUv = uv;
-				gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
-
-		}`,
-
-	fragmentShader: /* glsl */`
-
-		uniform sampler2D tDiffuse;
-		uniform float pixelSize;
-		uniform vec2 resolution;
-
-		varying highp vec2 vUv;
-
-		void main(){
-
-			vec2 dxy = pixelSize / resolution;
-			vec2 coord = dxy * floor( vUv / dxy );
-			gl_FragColor = texture2D(tDiffuse, coord);
-
-		}`
-
-};
-
-export { PixelShader };

二进制
examples/screenshots/webgl_postprocessing_pixel.jpg


二进制
examples/textures/checker.png


+ 171 - 125
examples/webgl_postprocessing_pixel.html

@@ -1,180 +1,226 @@
 <!DOCTYPE html>
 <html lang="en">
-	<head>
-		<title>three.js webgl - postprocessing - pixel shader</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="container"></div>
-
-		<div id="info">
-			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - pixel shader by <a href="http://wongbryan.github.io">wongbryan</a>
-		</div>
-
-		<!-- Import maps polyfill -->
-		<!-- Remove this when import maps will be widely supported -->
-		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
-
-		<script type="importmap">
-			{
-				"imports": {
-					"three": "../build/three.module.js",
-					"three/addons/": "./jsm/"
-				}
+
+<head>
+	<title>three.js webgl - post processing - pixelation</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> - 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>
+
+	<!-- Import maps polyfill -->
+	<!-- Remove this when import maps will be widely supported -->
+	<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
+
+	<script type="importmap">
+		{
+			"imports": {
+				"three": "../build/three.module.js",
+				"three/addons/": "./jsm/"
 			}
-		</script>
+		}
+	</script>
 
-		<script type="module">
+	<script type="module">
 
-			import * as THREE from 'three';
+		import * as THREE from 'three';
 
-			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+		import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+		import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+		import { RenderPixelatedPass } from 'three/addons/postprocessing/RenderPixelatedPass.js';
+		import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
-			import { TrackballControls } from 'three/addons/controls/TrackballControls.js';
-			import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
-			import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
-			import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
-			import { PixelShader } from 'three/addons/shaders/PixelShader.js';
+		let camera, scene, renderer, composer, crystalMesh, clock;
+		let gui, params;
 
-			let camera, scene, renderer, gui, composer, controls;
-			let pixelPass, params;
+		init();
+		animate();
 
-			let group;
+		function init() {
 
-			init();
-			animate();
+			const aspectRatio = window.innerWidth / window.innerHeight;
 
-			function updateGUI() {
+			camera = new THREE.OrthographicCamera( - aspectRatio, aspectRatio, 1, - 1, 0.1, 10 );
+			camera.position.y = 2 * Math.tan( Math.PI / 6 );
+			camera.position.z = 2;
 
-				pixelPass.uniforms[ 'pixelSize' ].value = params.pixelSize;
+			scene = new THREE.Scene();
+			scene.background = new THREE.Color( 0x151729 );
 
-			}
+			clock = new THREE.Clock();
 
-			function init() {
+			renderer = new THREE.WebGLRenderer();
+			renderer.shadowMap.enabled = true;
+			//renderer.setPixelRatio( window.devicePixelRatio );
+			renderer.setSize( window.innerWidth, window.innerHeight );
+			document.body.appendChild( renderer.domElement );
 
-				const container = document.getElementById( 'container' );
-				renderer = new THREE.WebGLRenderer( { antialias: true } );
-				renderer.setPixelRatio( window.devicePixelRatio );
-				renderer.setSize( window.innerWidth, window.innerHeight );
-				container.appendChild( renderer.domElement );
+			composer = new EffectComposer( renderer );
+			const renderPixelatedPass = new RenderPixelatedPass( 6, scene, camera );
+			composer.addPass( renderPixelatedPass );
 
-				camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 10000 );
-				camera.position.set( 0, 0, 30 );
-				controls = new TrackballControls( camera, renderer.domElement );
-				controls.rotateSpeed = 2.0;
-				controls.panSpeed = 0.8;
-				controls.zoomSpeed = 1.5;
+			window.addEventListener( 'resize', onWindowResize );
 
-				scene = new THREE.Scene();
+			const controls = new OrbitControls( camera, renderer.domElement );
+			controls.maxZoom = 2;
 
-				const hemisphereLight = new THREE.HemisphereLight( 0xfceafc, 0x000000, .8 );
-				scene.add( hemisphereLight );
+			// gui
 
-				const dirLight = new THREE.DirectionalLight( 0xffffff, .5 );
-				dirLight.position.set( 150, 75, 150 );
-				scene.add( dirLight );
+			gui = new GUI();
+			params = { pixelSize: 6, normalEdgeStrength: .3, depthEdgeStrength: .4 };
+			gui.add( params, 'pixelSize' ).min( 1 ).max( 16 ).step( 1 )
+				.onChange( () => {
 
-				const dirLight2 = new THREE.DirectionalLight( 0xffffff, .2 );
-				dirLight2.position.set( - 150, 75, - 150 );
-				scene.add( dirLight2 );
+					renderPixelatedPass.setPixelSize( params.pixelSize );
+		
+				} );
+			gui.add( renderPixelatedPass, 'normalEdgeStrength' ).min( 0 ).max( 2 ).step( .05 );
+			gui.add( renderPixelatedPass, 'depthEdgeStrength' ).min( 0 ).max( 1 ).step( .05 );
 
-				const dirLight3 = new THREE.DirectionalLight( 0xffffff, .1 );
-				dirLight3.position.set( 0, 125, 0 );
-				scene.add( dirLight3 );
+			// textures
 
-				const geometries = [
-					new THREE.SphereGeometry( 1, 64, 64 ),
-					new THREE.BoxGeometry( 1, 1, 1 ),
-					new THREE.ConeGeometry( 1, 1, 32 ),
-					new THREE.TetrahedronGeometry( 1 ),
-					new THREE.TorusKnotGeometry( 1, .4 )
-				];
+			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 );
 
-				group = new THREE.Group();
+			// meshes
 
-				for ( let i = 0; i < 25; i ++ ) {
+			const boxMaterial = new THREE.MeshPhongMaterial( { map: texChecker2 } );
+		
+			function addBox( boxSideLength, x, z, rotation ) {
 
-					const geom = geometries[ Math.floor( Math.random() * geometries.length ) ];
+				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;
+		
+			}
 
-					const color = new THREE.Color();
-					color.setHSL( Math.random(), .7 + .2 * Math.random(), .5 + .1 * Math.random() );
+			addBox( .4, 0, 0, Math.PI / 4 );
+			addBox( .5, - .5, - .5, Math.PI / 4 );
 
-					const mat = new THREE.MeshPhongMaterial( { color: color, shininess: 200 } );
+			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 mesh = new THREE.Mesh( geom, mat );
+			const radius = .2;
+			const geometry = new THREE.IcosahedronGeometry( radius );
+			crystalMesh = new THREE.Mesh(
+				geometry,
+				new THREE.MeshPhongMaterial( {
+					color: 0x2379cf,
+					emissive: 0x143542,
+					shininess: 10,
+					specular: 0xffffff
+				} )
+			);
+			crystalMesh.receiveShadow = true;
+			crystalMesh.castShadow = true;
+			scene.add( crystalMesh );
 
-					const s = 4 + Math.random() * 10;
-					mesh.scale.set( s, s, s );
-					mesh.position.set( Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5 ).normalize();
-					mesh.position.multiplyScalar( Math.random() * 200 );
-					mesh.rotation.set( Math.random() * 2, Math.random() * 2, Math.random() * 2 );
-					group.add( mesh );
+			// lights
 
-				}
+			scene.add( new THREE.AmbientLight( 0x2d3645, 1.5 ) );
 
-				scene.add( group );
+			const directionalLight = new THREE.DirectionalLight( 0xfffc9c, .5 );
+			directionalLight.position.set( 100, 100, 100 );
+			directionalLight.castShadow = true;
+			directionalLight.shadow.mapSize.set( 2048, 2048 );
+			scene.add( directionalLight );
 
-				composer = new EffectComposer( renderer );
-				composer.addPass( new RenderPass( scene, camera ) );
+			const spotLight = new THREE.SpotLight( 0xff8800, 1, 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 );
 
-				pixelPass = new ShaderPass( PixelShader );
-				pixelPass.uniforms[ 'resolution' ].value = new THREE.Vector2( window.innerWidth, window.innerHeight );
-				pixelPass.uniforms[ 'resolution' ].value.multiplyScalar( window.devicePixelRatio );
-				composer.addPass( pixelPass );
+		}
 
-				window.addEventListener( 'resize', onWindowResize );
+		function onWindowResize() {
 
-				params = {
-					pixelSize: 16,
-					postprocessing: true
-				};
-				gui = new GUI();
-				gui.add( params, 'pixelSize' ).min( 2 ).max( 32 ).step( 2 );
-				gui.add( params, 'postprocessing' );
+			const aspectRatio = window.innerWidth / window.innerHeight;
+			camera.left = - aspectRatio;
+			camera.right = aspectRatio;
+			camera.updateProjectionMatrix();
 
-			}
+			renderer.setSize( window.innerWidth, window.innerHeight );
+			composer.setSize( window.innerWidth, window.innerHeight );
 
-			function onWindowResize() {
+		}
 
-				camera.aspect = window.innerWidth / window.innerHeight;
-				camera.updateProjectionMatrix();
-				renderer.setSize( window.innerWidth, window.innerHeight );
+		function animate() {
 
-				pixelPass.uniforms[ 'resolution' ].value.set( window.innerWidth, window.innerHeight ).multiplyScalar( window.devicePixelRatio );
+			requestAnimationFrame( 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;
 
-			function update() {
+			composer.render();
 
-				controls.update();
-				updateGUI();
+		}
 
-				group.rotation.y += .0015;
-				group.rotation.z += .001;
+		// Helper functions
 
-			}
+		function pixelTexture( texture ) {
 
-			function animate() {
+			texture.minFilter = THREE.NearestFilter;
+			texture.magFilter = THREE.NearestFilter;
+			texture.generateMipmaps = false;
+			texture.wrapS = THREE.RepeatWrapping;
+			texture.wrapT = THREE.RepeatWrapping;
+			return texture;
 
-				update();
+		}
 
-				if ( params.postprocessing ) {
+		function easeInOutCubic( x ) {
 
-					composer.render();
+			return x ** 2 * 3 - x ** 3 * 2;
 
-				} else {
+		}
 
-					renderer.render( scene, camera );
+		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 );
 
-				window.requestAnimationFrame( animate );
+		}
 
-			}
+		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;
+
+		}
+		
+	</script>
+</body>
 
-		</script>
-	</body>
 </html>