Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 46 additions & 9 deletions src/renderers/WebGLRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2201,12 +2201,17 @@ class WebGLRenderer {

}

function getProgram( material, scene, object ) {
function getProgram( material, scene, object, isOutputVariantLookup = false ) {

if ( scene.isScene !== true ) scene = _emptyScene; // scene could be a Mesh, Line, Points, ...

const materialProperties = properties.get( material );

// a lookup for any other reason than filling the output variant map may change any of
// the state a remembered output variant program was found for, so drop the remembered
// programs: they no longer necessarily match the current state
if ( isOutputVariantLookup === false && materialProperties.outputPrograms !== undefined ) materialProperties.outputPrograms.clear();

const lights = currentRenderState.state.lights;
const shadowsArray = currentRenderState.state.shadowsArray;

Expand Down Expand Up @@ -2446,10 +2451,6 @@ class WebGLRenderer {

needsProgramChange = true;

} else if ( materialProperties.outputColorSpace !== colorSpace ) {

needsProgramChange = true;

} else if ( object.isBatchedMesh && materialProperties.batching === false ) {

needsProgramChange = true;
Expand Down Expand Up @@ -2532,10 +2533,6 @@ class WebGLRenderer {

needsProgramChange = true;

} else if ( materialProperties.toneMapping !== toneMapping ) {

needsProgramChange = true;

} else if ( materialProperties.morphTargetsCount !== morphTargetsCount ) {

needsProgramChange = true;
Expand Down Expand Up @@ -2569,6 +2566,46 @@ class WebGLRenderer {

}

} else if ( material.isNodeMaterial !== true && ( materialProperties.outputColorSpace !== colorSpace || materialProperties.toneMapping !== toneMapping ) ) {

// Only the output color space or the tone mapping differs from the state the current
// program was found for. This happens when a transmissive object makes the transmission
// pass draw the opaque objects into a render target (working color space, no tone
// mapping) before the main pass draws them with the renderer's output settings. Switch
// to the program remembered for this output variant instead of doing a full program lookup.

const outputKey = colorSpace + ':' + toneMapping;

let outputPrograms = materialProperties.outputPrograms;

if ( outputPrograms === undefined ) {

outputPrograms = new Map();
materialProperties.outputPrograms = outputPrograms;

}

const outputProgram = outputPrograms.get( outputKey );

if ( outputProgram !== undefined ) {

program = outputProgram;

materialProperties.currentProgram = program;
materialProperties.uniformsList = null; // rebuilt for the switched program

materialProperties.outputColorSpace = colorSpace;
materialProperties.toneMapping = toneMapping;

} else {

// first draw with this output variant: do a full lookup and remember the program

program = getProgram( material, scene, object, true );
outputPrograms.set( outputKey, program );

}

}

let refreshProgram = false;
Expand Down
140 changes: 140 additions & 0 deletions test/unit/src/renderers/WebGLRenderer.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { ACESFilmicToneMapping } from '../../../../src/constants.js';
import { PerspectiveCamera } from '../../../../src/cameras/PerspectiveCamera.js';
import { DirectionalLight } from '../../../../src/lights/DirectionalLight.js';
import { BoxGeometry } from '../../../../src/geometries/BoxGeometry.js';
import { SphereGeometry } from '../../../../src/geometries/SphereGeometry.js';
import { Mesh } from '../../../../src/objects/Mesh.js';
import { MeshPhysicalMaterial } from '../../../../src/materials/MeshPhysicalMaterial.js';
import { MeshStandardMaterial } from '../../../../src/materials/MeshStandardMaterial.js';
import { Scene } from '../../../../src/scenes/Scene.js';
import { WebGLRenderer } from '../../../../src/renderers/WebGLRenderer.js';


function createTransmissionScene() {

// one opaque and one transmissive object: the transmission pass draws the opaque
// object into a render target (working color space, no tone mapping) before the
// main pass draws it with the renderer's output settings

const scene = new Scene();

const material = new MeshStandardMaterial();

// getParameters() runs material.customProgramCacheKey() once per program lookup
const customProgramCacheKey = material.customProgramCacheKey;
let programLookups = 0;

material.customProgramCacheKey = function () {

programLookups ++;
return customProgramCacheKey.call( this );

};

const mesh = new Mesh( new BoxGeometry( 1, 1, 1 ), material );
mesh.position.z = - 2;
scene.add( mesh );

const transmissiveMesh = new Mesh( new SphereGeometry( 0.5, 16, 8 ), new MeshPhysicalMaterial( { transmission: 1 } ) );
transmissiveMesh.position.z = - 2;
scene.add( transmissiveMesh );

return {
scene,
countProgramLookups: function () {

return programLookups;

},
resetProgramLookups: function () {

programLookups = 0;

}
};

}

function createRenderer() {

const canvas = document.createElement( 'canvas' );

const renderer = new WebGLRenderer( { canvas } );
renderer.setSize( 64, 64, false );
renderer.toneMapping = ACESFilmicToneMapping;

return renderer;

}

export default QUnit.module( 'Renderers', () => {

QUnit.module( 'WebGLRenderer', () => {

QUnit.test( 'no repeated program lookups with a transmissive object in view', ( assert ) => {

const setup = createTransmissionScene();
const renderer = createRenderer();
const camera = new PerspectiveCamera();

// warm up until both output variants of the opaque material have their programs
// (render target pass and canvas pass)

renderer.render( setup.scene, camera );
renderer.render( setup.scene, camera );
renderer.render( setup.scene, camera );
renderer.render( setup.scene, camera );

setup.resetProgramLookups();

// steady frame: the opaque material is drawn once with the render target's
// output settings and once with the canvas' output settings

renderer.render( setup.scene, camera );

assert.equal( setup.countProgramLookups(), 0, 'no program lookups in a steady frame' );

renderer.dispose();

} );

QUnit.test( 'changing the lights state runs program lookups again', ( assert ) => {

const setup = createTransmissionScene();
const renderer = createRenderer();
const camera = new PerspectiveCamera();

renderer.render( setup.scene, camera );
renderer.render( setup.scene, camera );
renderer.render( setup.scene, camera );
renderer.render( setup.scene, camera );

// a lights state change must not keep serving remembered programs

setup.scene.add( new DirectionalLight( 0xffffff, 1 ) );

setup.resetProgramLookups();
renderer.render( setup.scene, camera );

assert.ok( setup.countProgramLookups() > 0, 'program lookups after a lights change' );

// each output variant re-runs one lookup for the new lights state; let the
// state settle, then a steady frame has no lookups again

setup.resetProgramLookups();
renderer.render( setup.scene, camera );
renderer.render( setup.scene, camera );
renderer.render( setup.scene, camera );

setup.resetProgramLookups();
renderer.render( setup.scene, camera );

assert.equal( setup.countProgramLookups(), 0, 'no program lookups once the new state settled' );

renderer.dispose();

} );

} );

} );
1 change: 1 addition & 0 deletions test/unit/three.source.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ import './src/objects/Sprite.tests.js';
import './src/renderers/WebGL3DRenderTarget.tests.js';
import './src/renderers/WebGLArrayRenderTarget.tests.js';
import './src/renderers/WebGLCubeRenderTarget.tests.js';
import './src/renderers/WebGLRenderer.tests.js';
import './src/renderers/WebGLRenderTarget.tests.js';

//src/renderers/shaders
Expand Down
Loading