diff --git a/examples/src/examples/graphics/render-to-texture.tsx b/examples/src/examples/graphics/render-to-texture.tsx index 1ff7cd48797..f4fba9e514c 100644 --- a/examples/src/examples/graphics/render-to-texture.tsx +++ b/examples/src/examples/graphics/render-to-texture.tsx @@ -18,10 +18,14 @@ class RenderToTextureExample { // - camera - this camera renders into main framebuffer, objects from World, Excluded and also Skybox layers // Create the app and start the update loop - const app = new pc.Application(canvas, {}); + const app = new pc.Application(canvas, { + mouse: new pc.Mouse(document.body), + touch: new pc.TouchDevice(document.body) + }); const assets = { - 'helipad.dds': new pc.Asset('helipad.dds', 'cubemap', { url: '/static/assets/cubemaps/helipad.dds' }, { type: pc.TEXTURETYPE_RGBM }) + 'helipad.dds': new pc.Asset('helipad.dds', 'cubemap', { url: '/static/assets/cubemaps/helipad.dds' }, { type: pc.TEXTURETYPE_RGBM }), + 'script': new pc.Asset('script', 'script', { url: '/static/scripts/camera/orbit-camera.js' }) }; const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets); @@ -55,6 +59,45 @@ class RenderToTextureExample { return primitive; } + // helper function to create a basic particle system + function createParticleSystem(position: pc.Vec3) { + + // make particles move in different directions + const localVelocityCurve = new pc.CurveSet([ + [0, 0, 0.5, 8], + [0, 0, 0.5, 8], + [0, 0, 0.5, 8] + ]); + const localVelocityCurve2 = new pc.CurveSet([ + [0, 0, 0.5, -8], + [0, 0, 0.5, -8], + [0, 0, 0.5, -8] + ]); + + // increasing gravity + const worldVelocityCurve = new pc.CurveSet([ + [0, 0], + [0, 0, 0.2, 6, 1, -48], + [0, 0] + ]); + + // Create entity for particle system + const entity = new pc.Entity(); + app.root.addChild(entity); + entity.setLocalPosition(position); + + // add particlesystem component to entity + entity.addComponent("particlesystem", { + numParticles: 200, + lifetime: 1, + rate: 0.01, + scaleGraph: new pc.Curve([0, 0.5]), + velocityGraph: worldVelocityCurve, + localVelocityGraph: localVelocityCurve, + localVelocityGraph2: localVelocityCurve2 + }); + } + // create texture and render target for rendering into, including depth buffer const texture = new pc.Texture(app.graphicsDevice, { width: 512, @@ -74,19 +117,22 @@ class RenderToTextureExample { samples: 2 }); - // create a layer for object that do not render into texture + // create a layer for object that do not render into texture, add it right after the world layer const excludedLayer = new pc.Layer({ name: "Excluded" }); - app.scene.layers.push(excludedLayer); + app.scene.layers.insert(excludedLayer, 1); // get world and skybox layers const worldLayer = app.scene.layers.getLayerByName("World"); const skyboxLayer = app.scene.layers.getLayerByName("Skybox"); // create ground plane and 3 primitives, visible in world layer - createPrimitive("plane", new pc.Vec3(0, 0, 0), new pc.Vec3(20, 20, 20), new pc.Color(0.2, 0.4, 0.2), [worldLayer.id]); + const plane = createPrimitive("plane", new pc.Vec3(0, 0, 0), new pc.Vec3(20, 20, 20), new pc.Color(0.2, 0.4, 0.2), [worldLayer.id]); createPrimitive("sphere", new pc.Vec3(-2, 1, 0), new pc.Vec3(2, 2, 2), pc.Color.RED, [worldLayer.id]); - createPrimitive("box", new pc.Vec3(2, 1, 0), new pc.Vec3(2, 2, 2), pc.Color.YELLOW, [worldLayer.id]); createPrimitive("cone", new pc.Vec3(0, 1, -2), new pc.Vec3(2, 2, 2), pc.Color.CYAN, [worldLayer.id]); + createPrimitive("box", new pc.Vec3(2, 1, 0), new pc.Vec3(2, 2, 2), pc.Color.YELLOW, [worldLayer.id]); + + // particle system + createParticleSystem(new pc.Vec3(2, 3, 0)); // Create main camera, which renders entities in world, excluded and skybox layers const camera = new pc.Entity("Camera"); @@ -98,6 +144,19 @@ class RenderToTextureExample { camera.lookAt(1, 4, 0); app.root.addChild(camera); + // add orbit camera script with a mouse and a touch support + camera.addComponent("script"); + camera.script.create("orbitCamera", { + attributes: { + inertiaFactor: 0.2, + focusEntity: plane, + distanceMax: 20, + frameOnStart: false + } + }); + camera.script.create("orbitCameraInputMouse"); + camera.script.create("orbitCameraInputTouch"); + // Create texture camera, which renders entities in world and skybox layers into the texture const textureCamera = new pc.Entity("TextureCamera"); textureCamera.addComponent("camera", { diff --git a/src/graphics/program-library.js b/src/graphics/program-library.js index 0b1d148661a..c28046e806f 100644 --- a/src/graphics/program-library.js +++ b/src/graphics/program-library.js @@ -47,11 +47,10 @@ class ProgramLibrary { getProgram(name, options) { const generator = this._generators[name]; - if (generator === undefined) { + if (!generator) { Debug.warn(`ProgramLibrary#getProgram: No program library functions registered for: ${name}`); return null; } - const gd = this._device; const key = generator.generateKey(options); let shader = this._cache[key]; if (!shader) { @@ -74,9 +73,10 @@ class ProgramLibrary { if (this._precached) console.warn(`ProgramLibrary#getProgram: Cache miss for shader ${name} key ${key} after shaders precaching`); - const shaderDefinition = generator.createShaderDefinition(gd, options); - shaderDefinition.name = `standard-pass:${options.pass}`; - shader = this._cache[key] = new Shader(gd, shaderDefinition); + const device = this._device; + const shaderDefinition = generator.createShaderDefinition(device, options); + shaderDefinition.name = `${name}-pass:${options.pass}`; + shader = this._cache[key] = new Shader(device, shaderDefinition); } return shader; } diff --git a/src/scene/layer.js b/src/scene/layer.js index 9c827476e07..ea12c086a92 100644 --- a/src/scene/layer.js +++ b/src/scene/layer.js @@ -607,10 +607,13 @@ class Layer { if (!skipShadowCasters && m.castShadow && casters.indexOf(m) < 0) casters.push(m); - if (!this.passThrough && sceneShaderVer >= 0 && mat._shaderVersion !== sceneShaderVer) { // clear old shader if needed - if (mat.updateShader !== Material.prototype.updateShader) { + // clear old shader variants if necessary + if (!this.passThrough && sceneShaderVer >= 0 && mat._shaderVersion !== sceneShaderVer) { + + // skip this for materials not using variants + if (mat.getShaderVariant !== Material.prototype.getShaderVariant) { + // clear shader variants on the material and also on mesh instances that use it mat.clearVariants(); - mat.shader = null; } mat._shaderVersion = sceneShaderVer; } diff --git a/src/scene/lightmapper/lightmapper.js b/src/scene/lightmapper/lightmapper.js index 1913f7a59de..56550c218b5 100644 --- a/src/scene/lightmapper/lightmapper.js +++ b/src/scene/lightmapper/lightmapper.js @@ -257,7 +257,6 @@ class Lightmapper { material.cull = CULLFACE_NONE; material.forceUv1 = true; // provide data to xformUv1 material.update(); - material.updateShader(device, scene); return material; } diff --git a/src/scene/materials/basic-material.js b/src/scene/materials/basic-material.js index becdf6f4875..a1249952f9d 100644 --- a/src/scene/materials/basic-material.js +++ b/src/scene/materials/basic-material.js @@ -1,3 +1,4 @@ +import { Debug } from '../../core/debug.js'; import { Color } from '../../math/color.js'; import { SHADERDEF_INSTANCING, SHADERDEF_MORPH_NORMAL, SHADERDEF_MORPH_POSITION, SHADERDEF_MORPH_TEXTURE_BASED, @@ -50,6 +51,15 @@ class BasicMaterial extends Material { this.vertexColors = false; } + set shader(shader) { + Debug.warn('BasicMaterial#shader property is not implemented, and should not be used.'); + } + + get shader() { + Debug.warn('BasicMaterial#shader property is not implemented, and should not be used.'); + return null; + } + /** * Copy a `BasicMaterial`. * @@ -79,7 +89,7 @@ class BasicMaterial extends Material { } } - updateShader(device, scene, objDefs, staticLightList, pass, sortedLights) { + getShaderVariant(device, scene, objDefs, staticLightList, pass, sortedLights) { const options = { skin: objDefs && (objDefs & SHADERDEF_SKIN) !== 0, screenSpace: objDefs && (objDefs & SHADERDEF_SCREENSPACE) !== 0, @@ -94,7 +104,7 @@ class BasicMaterial extends Material { pass: pass }; const library = device.getProgramLibrary(); - this.shader = library.getProgram('basic', options); + return library.getProgram('basic', options); } } diff --git a/src/scene/materials/material.js b/src/scene/materials/material.js index 7cf391dd25c..a82a1a466bf 100644 --- a/src/scene/materials/material.js +++ b/src/scene/materials/material.js @@ -17,6 +17,7 @@ import { Debug } from '../../core/debug.js'; import { getDefaultMaterial } from './default-material.js'; /** @typedef {import('../../graphics/texture.js').Texture} Texture */ +/** @typedef {import('../../graphics/shader.js').Shader} Shader */ let id = 0; @@ -114,6 +115,15 @@ let id = 0; * slope of the triangle relative to the camera. */ class Material { + /** + * A shader used to render the material. Note that this is used only by materials where the user + * specifies the shader. Most material types generate multiple shader variants, and do not set this. + * + * @type {Shader} + * @private + */ + _shader = null; + /** * Create a new Material instance. */ @@ -121,7 +131,6 @@ class Material { this.name = 'Untitled'; this.id = id++; - this._shader = null; this.variants = {}; this.parameters = {}; @@ -311,7 +320,7 @@ class Material { */ copy(source) { this.name = source.name; - this.shader = source.shader; + this._shader = source._shader; // Render states this.alphaTest = source.alphaTest; @@ -371,8 +380,10 @@ class Material { updateUniforms(device, scene) { } - updateShader(device, scene, objDefs, staticLightList, pass, sortedLights) { - // For vanilla materials, the shader can only be set by the user + getShaderVariant(device, scene, objDefs, staticLightList, pass, sortedLights) { + // return the shader specified by the user of the material + Debug.assert(this._shader, 'Material does not have shader set', this); + return this._shader; } /** @@ -393,11 +404,16 @@ class Material { } clearVariants() { + + // clear variants on the material this.variants = {}; + + // but also clear them from all materials that reference them for (let i = 0; i < this.meshInstances.length; i++) { const meshInstance = this.meshInstances[i]; - for (let j = 0; j < meshInstance._shader.length; j++) { - meshInstance._shader[j] = null; + const shaders = meshInstance._shader; + for (let j = 0; j < shaders.length; j++) { + shaders[j] = null; } } } diff --git a/src/scene/materials/standard-material.js b/src/scene/materials/standard-material.js index 2a000b23177..ab93ab007da 100644 --- a/src/scene/materials/standard-material.js +++ b/src/scene/materials/standard-material.js @@ -1,3 +1,5 @@ +import { Debug } from '../../core/debug.js'; + import { Color } from '../../math/color.js'; import { Vec2 } from '../../math/vec2.js'; import { Quat } from '../../math/quat.js'; @@ -506,6 +508,15 @@ class StandardMaterial extends Material { this._uniformCache = { }; } + set shader(shader) { + Debug.warn('StandardMaterial#shader property is not implemented, and should not be used.'); + } + + get shader() { + Debug.warn('StandardMaterial#shader property is not implemented, and should not be used.'); + return null; + } + /** * Object containing custom shader chunks that will replace default ones. * @@ -689,7 +700,6 @@ class StandardMaterial extends Material { this._processParameters('_activeParams'); if (this._dirtyShader) { - this.shader = null; this.clearVariants(); } } @@ -706,7 +716,7 @@ class StandardMaterial extends Material { this._processParameters('_activeLightingParams'); } - updateShader(device, scene, objDefs, staticLightList, pass, sortedLights) { + getShaderVariant(device, scene, objDefs, staticLightList, pass, sortedLights) { // update prefiltered lighting data this.updateEnvUniforms(device, scene); @@ -719,19 +729,16 @@ class StandardMaterial extends Material { else this.shaderOptBuilder.updateRef(options, scene, this, objDefs, staticLightList, pass, sortedLights); + // execute user callback to modify the options if (this.onUpdateShader) { options = this.onUpdateShader(options); } const library = device.getProgramLibrary(); - this.shader = library.getProgram('standard', options); - - if (!objDefs) { - this.clearVariants(); - this.variants[0] = this.shader; - } + const shader = library.getProgram('standard', options); this._dirtyShader = false; + return shader; } /** diff --git a/src/scene/mesh-instance.js b/src/scene/mesh-instance.js index 5c97489ea5d..696a9384628 100644 --- a/src/scene/mesh-instance.js +++ b/src/scene/mesh-instance.js @@ -22,6 +22,7 @@ import { LightmapCache } from './lightmapper/lightmap-cache.js'; /** @typedef {import('../math/vec3.js').Vec3} Vec3 */ /** @typedef {import('./materials/material.js').Material} Material */ /** @typedef {import('./mesh.js').Mesh} Mesh */ +/** @typedef {import('./scene.js').Scene} Scene */ /** @typedef {import('./morph-instance.js').MorphInstance} MorphInstance */ /** @typedef {import('./skin-instance.js').SkinInstance} SkinInstance */ @@ -654,6 +655,19 @@ class MeshInstance { } } + /** + * Obtain a shader variant required to render the mesh instance within specified pass. + * + * @param {Scene} scene - The scene. + * @param {number} pass - The render pass. + * @param {any} staticLightList - List of static lights. + * @param {any} sortedLights - Array of array of lights. + * @ignore + */ + updatePassShader(scene, pass, staticLightList, sortedLights) { + this._shader[pass] = this.material.getShaderVariant(this.mesh.device, scene, this._shaderDefs, staticLightList, pass, sortedLights); + } + // Parameter management clearParameters() { this.parameters = {}; diff --git a/src/scene/particle-system/particle-emitter.js b/src/scene/particle-system/particle-emitter.js index 0138e85d274..08f55cbd67a 100644 --- a/src/scene/particle-system/particle-emitter.js +++ b/src/scene/particle-system/particle-emitter.js @@ -833,8 +833,8 @@ class ParticleEmitter { if (this.lighting) { this.normalOption = hasNormal ? 2 : 1; } - // updateShader is also called by pc.Scene when all shaders need to be updated - this.material.updateShader = function () { + // getShaderVariant is also called by pc.Scene when all shaders need to be updated + this.material.getShaderVariant = function () { // The app works like this: // 1. Emitter init @@ -878,9 +878,10 @@ class ParticleEmitter { pack8: this.emitter.pack8, customFace: this.emitter.orientation !== PARTICLEORIENTATION_SCREEN }); - this.shader = shader; + + return shader; }; - this.material.updateShader(); + this.material.shader = this.material.getShaderVariant(); } resetMaterial() { diff --git a/src/scene/renderer/forward-renderer.js b/src/scene/renderer/forward-renderer.js index c397afec7d3..5dcbe9f85a0 100644 --- a/src/scene/renderer/forward-renderer.js +++ b/src/scene/renderer/forward-renderer.js @@ -51,6 +51,7 @@ import { WorldClustersDebug } from '../lighting/world-clusters-debug.js'; /** @typedef {import('../../framework/components/camera/component.js').CameraComponent} CameraComponent */ /** @typedef {import('../layer.js').Layer} Layer */ /** @typedef {import('../scene.js').Scene} Scene */ +/** @typedef {import('../mesh-instance.js').MeshInstance} MeshInstance */ /** @typedef {import('../camera.js').Camera} Camera */ /** @typedef {import('../frame-graph.js').FrameGraph} FrameGraph */ /** @typedef {import('../composition/layer-composition.js').LayerComposition} LayerComposition */ @@ -1145,19 +1146,6 @@ class ForwardRenderer { } } - updateShader(meshInstance, objDefs, staticLightList, pass, sortedLights) { - const material = meshInstance.material; - material._scene = this.scene; - - // if material has dirtyBlend set, notify scene here - if (material._dirtyBlend) { - this.scene.layers._dirtyBlend = true; - } - - material.updateShader(this.device, this.scene, objDefs, staticLightList, pass, sortedLights); - meshInstance._shader[pass] = material.shader; - } - setCullMode(cullFaces, flip, drawCall) { const material = drawCall.material; let mode = CULLFACE_NONE; @@ -1282,6 +1270,7 @@ class ForwardRenderer { for (let i = 0; i < drawCallsCount; i++) { + /** @type {MeshInstance} */ const drawCall = drawCalls[i]; // apply visibility override @@ -1325,26 +1314,33 @@ class ForwardRenderer { if (material !== prevMaterial) { this._materialSwitches++; + material._scene = scene; if (material.dirty) { material.updateUniforms(device, scene); material.dirty = false; } + // if material has dirtyBlend set, notify scene here + if (material._dirtyBlend) { + scene.layers._dirtyBlend = true; + } + if (!drawCall._shader[pass] || drawCall._shaderDefs !== objDefs || drawCall._lightHash !== lightHash) { if (!drawCall.isStatic) { const variantKey = pass + '_' + objDefs + '_' + lightHash; drawCall._shader[pass] = material.variants[variantKey]; if (!drawCall._shader[pass]) { - this.updateShader(drawCall, objDefs, null, pass, sortedLights); + drawCall.updatePassShader(scene, pass, null, sortedLights); material.variants[variantKey] = drawCall._shader[pass]; } } else { - this.updateShader(drawCall, objDefs, drawCall._staticLightList, pass, sortedLights); + drawCall.updatePassShader(scene, pass, drawCall._staticLightList, sortedLights); } - drawCall._shaderDefs = objDefs; drawCall._lightHash = lightHash; } + + Debug.assert(drawCall._shader[pass], "no shader for pass", material); } addCall(drawCall, material !== prevMaterial, !prevMaterial || lightMask !== prevLightMask); @@ -1536,6 +1532,10 @@ class ForwardRenderer { // #endif } + /** + * @param {MeshInstance[]} drawCalls - Mesh instances. + * @param {boolean} onlyLitShaders - Limits the update to shaders affected by lighting. + */ updateShaders(drawCalls, onlyLitShaders) { const count = drawCalls.length; for (let i = 0; i < count; i++) { @@ -1545,7 +1545,8 @@ class ForwardRenderer { if (!_tempMaterialSet.has(mat)) { _tempMaterialSet.add(mat); - if (mat.updateShader !== Material.prototype.updateShader) { + // skip this for materials not using variants + if (mat.getShaderVariant !== Material.prototype.getShaderVariant) { if (onlyLitShaders) { // skip materials not using lighting @@ -1553,8 +1554,8 @@ class ForwardRenderer { continue; } + // clear shader variants on the material and also on mesh instances that use it mat.clearVariants(); - mat.shader = null; } } } @@ -1564,6 +1565,10 @@ class ForwardRenderer { _tempMaterialSet.clear(); } + /** + * @param {LayerComposition} comp - The layer composition to update. + * @param {boolean} lightsChanged - True if lights of the composition has changed. + */ beginFrame(comp, lightsChanged) { const meshInstances = comp._meshInstances; @@ -1579,6 +1584,7 @@ class ForwardRenderer { // Update all skin matrices to properly cull skinned objects (but don't update rendering data yet) this.updateCpuSkinMatrices(meshInstances); + // clear mesh instance visibility const miCount = meshInstances.length; for (let i = 0; i < miCount; i++) { meshInstances[i].visibleThisFrame = false; diff --git a/src/scene/renderer/shadow-renderer.js b/src/scene/renderer/shadow-renderer.js index 4a43f7cdab4..cc49efb8dab 100644 --- a/src/scene/renderer/shadow-renderer.js +++ b/src/scene/renderer/shadow-renderer.js @@ -479,6 +479,7 @@ class ShadowRenderer { const device = this.device; const forwardRenderer = this.forwardRenderer; + const scene = forwardRenderer.scene; const passFlags = 1 << SHADER_SHADOW; // Sort shadow casters @@ -496,7 +497,7 @@ class ShadowRenderer { forwardRenderer.setSkinning(device, meshInstance, material); if (material.dirty) { - material.updateUniforms(device, forwardRenderer.scene); + material.updateUniforms(device, scene); material.dirty = false; } @@ -514,7 +515,7 @@ class ShadowRenderer { // set shader let shadowShader = meshInstance._shader[shadowPass]; if (!shadowShader) { - forwardRenderer.updateShader(meshInstance, meshInstance._shaderDefs, null, shadowPass); + meshInstance.updatePassShader(scene, shadowPass); shadowShader = meshInstance._shader[shadowPass]; meshInstance._key[SORTKEY_DEPTH] = getDepthKey(meshInstance); } diff --git a/src/scene/sky.js b/src/scene/sky.js index 4b7f6e3123f..59c2213a2c1 100644 --- a/src/scene/sky.js +++ b/src/scene/sky.js @@ -43,11 +43,11 @@ class Sky { const material = new Material(); - material.updateShader = function (dev, sc, defs, staticLightList, pass) { + material.getShaderVariant = function (dev, sc, defs, staticLightList, pass) { const library = device.getProgramLibrary(); if (texture.cubemap) { - this.shader = library.getProgram('skybox', { + return library.getProgram('skybox', { type: 'cubemap', rgbm: texture.type === TEXTURETYPE_RGBM, hdr: (texture.type === TEXTURETYPE_RGBM || texture.format === PIXELFORMAT_RGBA32F), @@ -57,18 +57,18 @@ class Sky { gamma: (pass === SHADER_FORWARDHDR ? (scene.gammaCorrection ? GAMMA_SRGBHDR : GAMMA_NONE) : scene.gammaCorrection), toneMapping: (pass === SHADER_FORWARDHDR ? TONEMAP_LINEAR : scene.toneMapping) }); - } else { - this.shader = library.getProgram('skybox', { - type: 'envAtlas', - encoding: texture.encoding, - useIntensity: scene.skyboxIntensity !== 1, - gamma: (pass === SHADER_FORWARDHDR ? (scene.gammaCorrection ? GAMMA_SRGBHDR : GAMMA_NONE) : scene.gammaCorrection), - toneMapping: (pass === SHADER_FORWARDHDR ? TONEMAP_LINEAR : scene.toneMapping) - }); } + + return library.getProgram('skybox', { + type: 'envAtlas', + encoding: texture.encoding, + useIntensity: scene.skyboxIntensity !== 1, + gamma: (pass === SHADER_FORWARDHDR ? (scene.gammaCorrection ? GAMMA_SRGBHDR : GAMMA_NONE) : scene.gammaCorrection), + toneMapping: (pass === SHADER_FORWARDHDR ? TONEMAP_LINEAR : scene.toneMapping) + }); }; - material.updateShader(); + material.shader = material.getShaderVariant(); if (texture.cubemap) { material.setParameter('texture_cubeMap', texture);