├── .gitignore
├── LICENSE
├── README.md
├── index.html
├── index.js
├── src
├── BatchedPropertiesTexture.js
├── BatchedStandardMaterial.js
└── RenderTarget2DArray.js
└── three.module.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Garrett Johnson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # batched-material-properties-demo
2 |
3 | Demonstration of rendering three.js' BatchedMesh with different material properties in one draw call by packing material properties into a single texture. The demo features 5000 individually animate-able geometries in a batched mesh with unique metalness, roughness, emissive, and diffuse color properties.
4 |
5 | Demo [here](https://gkjohnson.github.io/batched-material-properties-demo/)
6 |
7 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three.js webgl - mesh - batch
5 |
6 |
7 |
24 |
25 |
26 |
27 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 |
2 | import * as THREE from 'three';
3 |
4 | import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
5 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
6 | import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
7 | import { BatchedStandardMaterial } from './src/BatchedStandardMaterial.js';
8 |
9 | let gui, infoEl;
10 | let camera, controls, scene, renderer;
11 | let geometries, mesh, material;
12 | const ids = [];
13 | const matrix = new THREE.Matrix4();
14 | const color = new THREE.Color();
15 | const clock = new THREE.Clock();
16 |
17 | //
18 |
19 | const position = new THREE.Vector3();
20 | const rotation = new THREE.Euler();
21 | const quaternion = new THREE.Quaternion();
22 | const scale = new THREE.Vector3();
23 |
24 | //
25 |
26 | const raycaster = new THREE.Raycaster();
27 | const mouse = new THREE.Vector2();
28 |
29 | //
30 |
31 | let frameTime = 0;
32 | let frameSamples = 0;
33 | let lastFrameStart = - 1;
34 | let averageTime = 0;
35 | let timeSamples = 0;
36 |
37 |
38 | const MAX_GEOMETRY_COUNT = 5000;
39 |
40 | const params = {
41 | animationSpeed: 1,
42 | metalness: true,
43 | roughness: true,
44 | emissive: true,
45 | color: true,
46 | checkShaderErrors: false,
47 | };
48 |
49 | init();
50 | initGeometries();
51 | initMesh();
52 | animate();
53 |
54 | //
55 |
56 | function rand( min, max ) {
57 |
58 | const delta = max - min;
59 | return min + Math.random() * delta;
60 |
61 | }
62 |
63 | function randomizeMatrix( matrix ) {
64 |
65 | position.randomDirection().multiplyScalar( 40 * Math.random() ** 1.5 );
66 |
67 | rotation.x = Math.random() * 2 * Math.PI;
68 | rotation.y = Math.random() * 2 * Math.PI;
69 | rotation.z = Math.random() * 2 * Math.PI;
70 |
71 | quaternion.setFromEuler( rotation );
72 |
73 | scale.x = scale.y = scale.z = 0.35 + ( Math.random() * 1.25 );
74 |
75 | return matrix.compose( position, quaternion, scale );
76 |
77 | }
78 |
79 | function randomizeRotationSpeed( rotation ) {
80 |
81 | rotation.x = Math.random() * 0.01;
82 | rotation.y = Math.random() * 0.01;
83 | rotation.z = Math.random() * 0.01;
84 | return rotation;
85 |
86 | }
87 |
88 | function initGeometries() {
89 |
90 | geometries = [
91 | new THREE.ConeGeometry( 1.0, 2.0 ).toNonIndexed(),
92 | new THREE.BoxGeometry( 2.0, 2.0, 2.0 ).toNonIndexed(),
93 | new THREE.IcosahedronGeometry( 1, 1 ),//( 1.0, 20, 15 ),
94 | new THREE.SphereGeometry( 1.0, 16, 13 ).toNonIndexed(),
95 | ];
96 |
97 | geometries[2].computeVertexNormals();
98 |
99 | }
100 |
101 | function initMesh() {
102 |
103 | const geometryCount = MAX_GEOMETRY_COUNT;
104 | const vertexCount = geometryCount * 512;
105 | const indexCount = geometryCount * 1024;
106 |
107 | const euler = new THREE.Euler();
108 | const matrix = new THREE.Matrix4();
109 | mesh = new THREE.BatchedMesh( geometryCount, vertexCount, indexCount, null );
110 | mesh.sortObjects = false;
111 | mesh.perObjectFrustumCulled = false;
112 | mesh.userData.info = [];
113 |
114 | updateMaterial();
115 |
116 | // disable full-object frustum culling since all of the objects can be dynamic.
117 | mesh.frustumCulled = false;
118 |
119 | ids.length = 0;
120 |
121 | for ( let i = 0; i < geometryCount; i ++ ) {
122 |
123 | const id = mesh.addGeometry( geometries[ i % geometries.length ] );
124 | mesh.setMatrixAt( id, randomizeMatrix( matrix ) );
125 |
126 | const rotationMatrix = new THREE.Matrix4();
127 | rotationMatrix.makeRotationFromEuler( randomizeRotationSpeed( euler ) );
128 |
129 | ids.push( id );
130 |
131 | const c0 = new THREE.Color();
132 | const c1 = new THREE.Color();
133 | c0.setHSL( rand( 0, 0.05 ), rand( 1, 1 ), rand( 0.5, 0.7 ) );
134 | c1.setHSL( rand( 0.5, 0.55 ), rand( 1, 1 ), rand( 0.5, 0.7 ) );
135 |
136 | const roughness = rand( 0, 0.5 );
137 | const metalness = rand( 0, 1 );
138 | let emissiveIntensity = 0;
139 | if ( rand( 0, 1 ) < 0.2 ) {
140 |
141 | emissiveIntensity = rand( 1, 5 );
142 |
143 | }
144 |
145 | mesh.userData.info.push( {
146 | rotationMatrix,
147 | c0,
148 | c1,
149 | emissiveIntensity,
150 | roughness,
151 | metalness,
152 | offset: rand( 0, 2 * Math.PI ),
153 | speed: rand( 1, 3 ),
154 | value: 0,
155 | } );
156 |
157 | }
158 |
159 | scene.add( mesh );
160 |
161 | }
162 |
163 | function updateMaterial() {
164 |
165 | const props = [];
166 | if ( params.metalness ) props.push( 'metalness' );
167 | if ( params.color ) props.push( 'diffuse' );
168 | if ( params.roughness ) props.push( 'roughness' );
169 | if ( params.emissive ) props.push( 'emissive' );
170 |
171 | if ( material ) material.dispose();
172 |
173 | material = new BatchedStandardMaterial( {}, MAX_GEOMETRY_COUNT, props );
174 | mesh.material = material;
175 |
176 | frameSamples = 0;
177 |
178 | }
179 |
180 | function init() {
181 |
182 | const width = window.innerWidth;
183 | const height = window.innerHeight;
184 |
185 | // camera
186 |
187 | camera = new THREE.PerspectiveCamera( 70, width / height, 1, 500 );
188 | camera.position.set( 50, 30, 30 ).multiplyScalar( 1.25 );
189 |
190 | // renderer
191 |
192 | renderer = new THREE.WebGLRenderer( { antialias: true } );
193 | renderer.setPixelRatio( window.devicePixelRatio );
194 | renderer.setSize( width, height );
195 | renderer.toneMapping = THREE.ACESFilmicToneMapping;
196 | document.body.appendChild( renderer.domElement );
197 |
198 | // scene
199 |
200 | scene = new THREE.Scene();
201 | scene.background = new THREE.Color( 0x182122 );
202 | scene.fog = new THREE.Fog( 0x222222, 60, 150 );
203 |
204 | const url = 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/equirectangular/royal_esplanade_1k.hdr';
205 | new RGBELoader()
206 | .load( url, texture => {
207 |
208 | texture.mapping = THREE.EquirectangularReflectionMapping;
209 |
210 | // scene.background = texture;
211 | scene.environment = texture;
212 |
213 | } );
214 |
215 | // controls
216 |
217 | controls = new OrbitControls( camera, renderer.domElement );
218 | controls.autoRotate = true;
219 | controls.autoRotateSpeed = 0.2;
220 |
221 | // gui
222 |
223 | gui = new GUI();
224 | gui.add( params, 'animationSpeed', 0, 3 ).step( 0.1 );
225 | gui.add( params, 'color' ).onChange( updateMaterial );
226 | gui.add( params, 'metalness' ).onChange( updateMaterial );
227 | gui.add( params, 'roughness' ).onChange( updateMaterial );
228 | gui.add( params, 'emissive' ).onChange( updateMaterial );
229 | gui.add( params, 'checkShaderErrors' );
230 |
231 | infoEl = document.getElementById( 'info' );
232 |
233 | // listeners
234 |
235 | window.addEventListener( 'resize', onWindowResize );
236 | window.addEventListener( 'mousemove', e => {
237 |
238 | mouse.set(
239 | ( e.clientX / window.innerWidth ) * 2 - 1,
240 | - ( e.clientY / window.innerHeight ) * 2 + 1
241 | );
242 |
243 | } );
244 |
245 | }
246 |
247 | //
248 |
249 | function onWindowResize() {
250 |
251 | const width = window.innerWidth;
252 | const height = window.innerHeight;
253 |
254 | camera.aspect = width / height;
255 | camera.updateProjectionMatrix();
256 |
257 | renderer.setSize( width, height );
258 |
259 | }
260 |
261 | function animate() {
262 |
263 | requestAnimationFrame( animate );
264 |
265 | animateMeshes();
266 |
267 | updateHover();
268 |
269 | controls.update();
270 |
271 | render();
272 |
273 | }
274 |
275 | function updateHover() {
276 |
277 | scene.updateMatrixWorld();
278 | camera.updateMatrixWorld();
279 | raycaster.setFromCamera( mouse, camera );
280 |
281 | const hit = raycaster.intersectObject( mesh )[ 0 ];
282 | if ( hit ) {
283 |
284 | const batchId = hit.batchId;
285 | material.setValue( batchId, 'diffuse', 0, 0, 0 );
286 | material.setValue( batchId, 'emissive', 1, 0.2, 0.1 );
287 | material.setValue( batchId, 'metalness', 0 );
288 | material.setValue( batchId, 'roughness', 1 );
289 |
290 | }
291 |
292 | }
293 |
294 | function animateMeshes() {
295 |
296 | const delta = clock.getDelta();
297 | for ( let i = 0; i < MAX_GEOMETRY_COUNT; i ++ ) {
298 |
299 | const info = mesh.userData.info[ i ];
300 | const {
301 | rotationMatrix,
302 | c0,
303 | c1,
304 | speed,
305 | offset,
306 | roughness,
307 | metalness,
308 | emissiveIntensity,
309 | } = info;
310 | const id = ids[ i ];
311 |
312 | mesh.getMatrixAt( id, matrix );
313 | matrix.multiply( rotationMatrix );
314 | mesh.setMatrixAt( id, matrix );
315 |
316 | info.value += delta * 2 * speed * params.animationSpeed;
317 | color.lerpColors( c0, c1, 0.5 + 0.5 * Math.sin( offset + info.value ) );
318 | material.setValue( i, 'diffuse', ...color );
319 | material.setValue( i, 'roughness', roughness );
320 | material.setValue( i, 'metalness', metalness );
321 | material.setValue( i, 'emissive', color.r * emissiveIntensity, color.g * emissiveIntensity, color.b * emissiveIntensity );
322 |
323 | }
324 |
325 | scene.fog.near = Math.min( camera.position.length() - 30, 80 );
326 | scene.fog.far = scene.fog.near + 60;
327 |
328 | }
329 |
330 | function render() {
331 |
332 | renderer.info.checkShaderErrors = params.checkShaderErrors;
333 |
334 | let frameDelta;
335 | if ( lastFrameStart === - 1 ) {
336 |
337 | lastFrameStart = window.performance.now();
338 |
339 | } else {
340 |
341 | frameDelta = window.performance.now() - lastFrameStart;
342 | frameTime += ( frameDelta - frameTime ) / ( frameSamples + 1 );
343 | if ( frameSamples < 60 ) {
344 |
345 | frameSamples ++
346 |
347 | }
348 |
349 | lastFrameStart = window.performance.now();
350 |
351 | }
352 |
353 | const start = window.performance.now();
354 | renderer.render( scene, camera );
355 | const delta = window.performance.now() - start;
356 | averageTime += ( delta - averageTime ) / ( timeSamples + 1 );
357 | if ( timeSamples < 60 ) {
358 |
359 | timeSamples ++;
360 |
361 | }
362 |
363 | infoEl.innerHTML = `draw calls : ${ renderer.info.render.calls }\n`;
364 | infoEl.innerHTML += `render time : ${ averageTime.toFixed( 2 ) }ms\n`;
365 | infoEl.innerHTML += `frame time : ${ frameTime.toFixed( 2 ) }ms`;
366 |
367 | }
368 |
--------------------------------------------------------------------------------
/src/BatchedPropertiesTexture.js:
--------------------------------------------------------------------------------
1 | import { DataTexture, FloatType, RGBAFormat } from 'three';
2 |
3 | function parseToInfo( type ) {
4 |
5 | let subtype = type;
6 | let dim = parseFloat( type.replace( /[^1-3]/g, '' ) ) || 1;
7 | if ( /$vec/.test( type ) ) subtype = 'float';
8 | if ( /$uvec/.test( type ) ) subtype = 'uint';
9 | if ( /$ivec/.test( type ) ) subtype = 'int';
10 |
11 | let comp;
12 | switch ( dim ) {
13 |
14 | case 1: comp = 'r'; break;
15 | case 2: comp = 'rg'; break;
16 | case 3: comp = 'rgb'; break;
17 | case 4: comp = 'rgba'; break;
18 |
19 | }
20 |
21 | return { type, subtype, dim, comp };
22 |
23 | }
24 |
25 | export class BatchedPropertiesTexture extends DataTexture {
26 |
27 | constructor( params, count ) {
28 |
29 | const fields = Object
30 | .entries( params )
31 | .map( ( [ name, type ] ) => ( {
32 | name,
33 | ...parseToInfo( type ),
34 | } ) )
35 | .sort( ( a, b ) => {
36 |
37 | return a.dim - b.dim;
38 |
39 | } );
40 |
41 | const width = fields.length;
42 | let size = Math.sqrt( count * width );
43 | size = Math.ceil( size / width ) * width;
44 | size = Math.max( size, width );
45 |
46 | const fieldToIndex = {};
47 | for ( let i = 0, l = fields.length; i < l; i ++ ) {
48 |
49 | fieldToIndex[ fields[ i ].name ] = i;
50 |
51 | }
52 |
53 | super( new Float32Array( size * size * 4 ), size, size, RGBAFormat, FloatType );
54 |
55 | this.fields = fields;
56 | this.fieldToIndex = fieldToIndex;
57 |
58 | }
59 |
60 | setValue( id, name, ...values ) {
61 |
62 | const { fields, fieldToIndex, image } = this;
63 | const width = fields.length;
64 | if ( ! ( name in fieldToIndex ) ) {
65 |
66 | return;
67 |
68 | }
69 |
70 | const fieldId = fieldToIndex[ name ];
71 | const field = fields[ fieldId ];
72 | const dim = field.dim;
73 | const data = image.data;
74 | const offset = id * width * 4 + fieldId * 4;
75 |
76 | for ( let i = 0; i < dim; i ++ ) {
77 |
78 | data[ offset + i ] = values[ i ] || 0;
79 |
80 | }
81 |
82 | this.needsUpdate = true;
83 |
84 | }
85 |
86 | getGlsl( idField = 'vBatchId', textureName = 'propertiesTex', indent = '' ) {
87 |
88 | const { fields, image } = this;
89 | const size = image.width;
90 | const width = fields.length;
91 |
92 | let result =
93 | `${ indent }int size = ${ size };\n` +
94 | `${ indent }int j = int( ${ idField } ) * ${ width };\n` +
95 | `${ indent }int x = j % size;\n` +
96 | `${ indent }int y = j / size;\n`;
97 |
98 | for ( let i = 0, l = fields.length; i < l; i ++ ) {
99 |
100 | const { name, type, comp } = fields[ i ];
101 | result += `${ indent }${ type } ${ name } = ${ type }( texelFetch( ${ textureName }, ivec2( x + ${ i }, y ), 0 ).${ comp } );\n`;
102 |
103 | }
104 |
105 | return result;
106 |
107 | }
108 |
109 | }
--------------------------------------------------------------------------------
/src/BatchedStandardMaterial.js:
--------------------------------------------------------------------------------
1 | import { MeshStandardMaterial } from 'three';
2 | import { BatchedPropertiesTexture } from './BatchedPropertiesTexture.js';
3 |
4 | const properties = {
5 | diffuse: 'vec3',
6 | emissive: 'vec3',
7 | metalness: 'float',
8 | roughness: 'float',
9 | };
10 |
11 | export class BatchedStandardMaterial extends MeshStandardMaterial {
12 |
13 | constructor( params, geometryCount, propertiesList = null ) {
14 |
15 | super( params );
16 |
17 | let props = { ...properties };
18 | if ( propertiesList ) {
19 |
20 | for ( const key in props ) {
21 |
22 | if ( ! propertiesList.includes( key ) ) {
23 |
24 | delete props[ key ];
25 |
26 | }
27 |
28 | }
29 |
30 | }
31 |
32 | const propertiesTex = new BatchedPropertiesTexture( props, geometryCount );
33 | this.propertiesTex = propertiesTex;
34 |
35 | this.onBeforeCompile = ( parameters, renderer ) => {
36 |
37 | if ( Object.keys( props ).length === 0 ) {
38 |
39 | return;
40 |
41 | }
42 |
43 | parameters.uniforms.propertiesTex = { value: propertiesTex };
44 |
45 | parameters.vertexShader = parameters
46 | .vertexShader
47 | .replace(
48 | 'void main() {',
49 | `
50 | varying float vBatchId;
51 | void main() {
52 |
53 | // add 0.5 to the value to avoid floating error that may cause flickering
54 | vBatchId = batchId + 0.5;
55 | `
56 | );
57 |
58 | parameters.fragmentShader = parameters
59 | .fragmentShader
60 | .replace(
61 | 'void main() {',
62 | `
63 | uniform highp sampler2D propertiesTex;
64 | varying float vBatchId;
65 | void main() {
66 |
67 | ${ propertiesTex.getGlsl() }
68 | `
69 | );
70 |
71 | };
72 |
73 | }
74 |
75 | setValue( ...args ) {
76 |
77 | this.propertiesTex.setValue( ...args );
78 |
79 | }
80 |
81 | dispose() {
82 |
83 | super.dispose();
84 | this.propertiesTex.dispose();
85 |
86 | }
87 |
88 | }
--------------------------------------------------------------------------------
/src/RenderTarget2DArray.js:
--------------------------------------------------------------------------------
1 | import {
2 | WebGLArrayRenderTarget,
3 | RGBAFormat,
4 | UnsignedByteType,
5 | MeshBasicMaterial,
6 | Color,
7 | RepeatWrapping,
8 | LinearFilter,
9 | NoToneMapping,
10 | DataTexture,
11 | ByteType,
12 | } from 'three';
13 | import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
14 |
15 | const whiteTex = new DataTexture( new Uint8Array( [ 255, 255, 255, 255 ] ), 1, 1, RGBAFormat, ByteType );
16 | const prevColor = new Color();
17 | export class RenderTarget2DArray extends WebGLArrayRenderTarget {
18 |
19 | constructor( renderer, ...args ) {
20 |
21 | super( ...args );
22 |
23 | this._renderer = renderer;
24 |
25 | const tex = this.texture;
26 | tex.format = RGBAFormat;
27 | tex.type = UnsignedByteType;
28 | tex.minFilter = LinearFilter;
29 | tex.magFilter = LinearFilter;
30 | tex.wrapS = RepeatWrapping;
31 | tex.wrapT = RepeatWrapping;
32 |
33 | const fsQuad = new FullScreenQuad( new MeshBasicMaterial() );
34 | this.fsQuad = fsQuad;
35 |
36 | }
37 |
38 | setTextureAt( id, texture ) {
39 |
40 | texture = texture || whiteTex;
41 |
42 | // save previous renderer state
43 | const renderer = this._renderer;
44 | const prevRenderTarget = renderer.getRenderTarget();
45 | const prevToneMapping = renderer.toneMapping;
46 | const prevAlpha = renderer.getClearAlpha();
47 | renderer.getClearColor( prevColor );
48 |
49 | // resize the render target and ensure we don't have an empty texture
50 | // render target depth must be >= 1 to avoid unbound texture error on android devices
51 | renderer.setClearColor( 0, 0 );
52 | renderer.toneMapping = NoToneMapping;
53 |
54 | // render each texture into each layer of the target
55 | const fsQuad = this.fsQuad;
56 |
57 | // revert to default texture transform before rendering
58 | texture.matrixAutoUpdate = false;
59 | texture.matrix.identity();
60 |
61 | fsQuad.material.map = texture;
62 | fsQuad.material.transparent = true;
63 |
64 | renderer.setRenderTarget( this, id );
65 | fsQuad.render( renderer );
66 |
67 | // restore custom texture transform
68 | texture.updateMatrix();
69 | texture.matrixAutoUpdate = true;
70 |
71 | // reset the renderer
72 | fsQuad.material.map = null;
73 | renderer.setClearColor( prevColor, prevAlpha );
74 | renderer.setRenderTarget( prevRenderTarget );
75 | renderer.toneMapping = prevToneMapping;
76 |
77 | }
78 |
79 | dispose() {
80 |
81 | super.dispose();
82 | this.fsQuad.dispose();
83 |
84 | }
85 |
86 | }
87 |
--------------------------------------------------------------------------------