// Light-pre pass deferred rendering // http://www.realtimerendering.com/blog/deferred-lighting-approaches/ import Base from '../core/Base'; import Shader from '../Shader'; import Material from '../Material'; import FrameBuffer from '../FrameBuffer'; import FullQuadPass from '../compositor/Pass'; import Texture2D from '../Texture2D'; import Texture from '../Texture'; import Mesh from '../Mesh'; import SphereGeo from '../geometry/Sphere'; import ConeGeo from '../geometry/Cone'; import CylinderGeo from '../geometry/Cylinder'; import Matrix4 from '../math/Matrix4'; import Vector3 from '../math/Vector3'; import GBuffer from './GBuffer'; import prezGlsl from '../shader/source/prez.glsl.js'; import utilGlsl from '../shader/source/util.glsl.js'; import lightvolumeGlsl from '../shader/source/deferred/lightvolume.glsl.js'; // Light shaders import spotGlsl from '../shader/source/deferred/spot.glsl.js'; import directionalGlsl from '../shader/source/deferred/directional.glsl.js'; import ambientGlsl from '../shader/source/deferred/ambient.glsl.js'; import ambientshGlsl from '../shader/source/deferred/ambientsh.glsl.js'; import ambientcubemapGlsl from '../shader/source/deferred/ambientcubemap.glsl.js'; import pointGlsl from '../shader/source/deferred/point.glsl.js'; import sphereGlsl from '../shader/source/deferred/sphere.glsl.js'; import tubeGlsl from '../shader/source/deferred/tube.glsl.js'; Shader.import(prezGlsl); Shader.import(utilGlsl); Shader.import(lightvolumeGlsl); // Light shaders Shader.import(spotGlsl); Shader.import(directionalGlsl); Shader.import(ambientGlsl); Shader.import(ambientshGlsl); Shader.import(ambientcubemapGlsl); Shader.import(pointGlsl); Shader.import(sphereGlsl); Shader.import(tubeGlsl); Shader.import(prezGlsl); /** * Deferred renderer * @constructor * @alias clay.deferred.Renderer * @extends clay.core.Base */ var DeferredRenderer = Base.extend(function () { var fullQuadVertex = Shader.source('clay.compositor.vertex'); var lightVolumeVertex = Shader.source('clay.deferred.light_volume.vertex'); var directionalLightShader = new Shader(fullQuadVertex, Shader.source('clay.deferred.directional_light')); var lightAccumulateBlendFunc = function (gl) { gl.blendEquation(gl.FUNC_ADD); gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE); }; var createLightPassMat = function (shader) { return new Material({ shader: shader, blend: lightAccumulateBlendFunc, transparent: true, depthMask: false }); }; var createVolumeShader = function (name) { return new Shader(lightVolumeVertex, Shader.source('clay.deferred.' + name)); }; // Rotate and positioning to fit the spot light // Which the cusp of cone pointing to the positive z // and positioned on the origin var coneGeo = new ConeGeo({ capSegments: 10 }); var mat = new Matrix4(); mat.rotateX(Math.PI / 2) .translate(new Vector3(0, -1, 0)); coneGeo.applyTransform(mat); var cylinderGeo = new CylinderGeo({ capSegments: 10 }); // Align with x axis mat.identity().rotateZ(Math.PI / 2); cylinderGeo.applyTransform(mat); return /** @lends clay.deferred.Renderer# */ { /** * Provide ShadowMapPass for shadow rendering. * @type {clay.prePass.ShadowMap} */ shadowMapPass: null, /** * If enable auto resizing from given defualt renderer size. * @type {boolean} */ autoResize: true, _createLightPassMat: createLightPassMat, _gBuffer: new GBuffer(), _lightAccumFrameBuffer: new FrameBuffer(), _lightAccumTex: new Texture2D({ // FIXME Device not support float texture type: Texture.HALF_FLOAT, minFilter: Texture.NEAREST, magFilter: Texture.NEAREST }), _fullQuadPass: new FullQuadPass({ blendWithPrevious: true }), _directionalLightMat: createLightPassMat(directionalLightShader), _ambientMat: createLightPassMat(new Shader( fullQuadVertex, Shader.source('clay.deferred.ambient_light') )), _ambientSHMat: createLightPassMat(new Shader( fullQuadVertex, Shader.source('clay.deferred.ambient_sh_light') )), _ambientCubemapMat: createLightPassMat(new Shader( fullQuadVertex, Shader.source('clay.deferred.ambient_cubemap_light') )), _spotLightShader: createVolumeShader('spot_light'), _pointLightShader: createVolumeShader('point_light'), _sphereLightShader: createVolumeShader('sphere_light'), _tubeLightShader: createVolumeShader('tube_light'), _lightSphereGeo: new SphereGeo({ widthSegments: 10, heightSegements: 10 }), _lightConeGeo: coneGeo, _lightCylinderGeo: cylinderGeo, _outputPass: new FullQuadPass({ fragment: Shader.source('clay.compositor.output') }) }; }, /** @lends clay.deferred.Renderer# */ { /** * Do render * @param {clay.Renderer} renderer * @param {clay.Scene} scene * @param {clay.Camera} camera * @param {Object} [opts] * @param {boolean} [opts.renderToTarget = false] If not ouput and render to the target texture * @param {boolean} [opts.notUpdateShadow = true] If not update the shadow. * @param {boolean} [opts.notUpdateScene = true] If not update the scene. */ render: function (renderer, scene, camera, opts) { opts = opts || {}; opts.renderToTarget = opts.renderToTarget || false; opts.notUpdateShadow = opts.notUpdateShadow || false; opts.notUpdateScene = opts.notUpdateScene || false; if (!opts.notUpdateScene) { scene.update(false, true); } scene.updateLights(); // Render list will be updated in gbuffer. camera.update(true); // PENDING For stereo rendering var dpr = renderer.getDevicePixelRatio(); if (this.autoResize && (renderer.getWidth() * dpr !== this._lightAccumTex.width || renderer.getHeight() * dpr !== this._lightAccumTex.height) ) { this.resize(renderer.getWidth() * dpr, renderer.getHeight() * dpr); } this._gBuffer.update(renderer, scene, camera); // Accumulate light buffer this._accumulateLightBuffer(renderer, scene, camera, !opts.notUpdateShadow); if (!opts.renderToTarget) { this._outputPass.setUniform('texture', this._lightAccumTex); this._outputPass.render(renderer); // this._gBuffer.renderDebug(renderer, camera, 'normal'); } }, /** * @return {clay.Texture2D} */ getTargetTexture: function () { return this._lightAccumTex; }, /** * @return {clay.FrameBuffer} */ getTargetFrameBuffer: function () { return this._lightAccumFrameBuffer; }, /** * @return {clay.deferred.GBuffer} */ getGBuffer: function () { return this._gBuffer; }, // TODO is dpr needed? setViewport: function (x, y, width, height, dpr) { this._gBuffer.setViewport(x, y, width, height, dpr); this._lightAccumFrameBuffer.viewport = this._gBuffer.getViewport(); }, // getFullQuadLightPass: function () { // return this._fullQuadPass; // }, /** * Set renderer size. * @param {number} width * @param {number} height */ resize: function (width, height) { this._lightAccumTex.width = width; this._lightAccumTex.height = height; // PENDING viewport ? this._gBuffer.resize(width, height); }, _accumulateLightBuffer: function (renderer, scene, camera, updateShadow) { var gl = renderer.gl; var lightAccumTex = this._lightAccumTex; var lightAccumFrameBuffer = this._lightAccumFrameBuffer; var eyePosition = camera.getWorldPosition().array; // Update volume meshes for (var i = 0; i < scene.lights.length; i++) { if (!scene.lights[i].invisible) { this._updateLightProxy(scene.lights[i]); } } var shadowMapPass = this.shadowMapPass; if (shadowMapPass && updateShadow) { gl.clearColor(1, 1, 1, 1); this._prepareLightShadow(renderer, scene, camera); } this.trigger('beforelightaccumulate', renderer, scene, camera, updateShadow); lightAccumFrameBuffer.attach(lightAccumTex); lightAccumFrameBuffer.bind(renderer); var clearColor = renderer.clearColor; var viewport = lightAccumFrameBuffer.viewport; if (viewport) { var dpr = viewport.devicePixelRatio; // use scissor to make sure only clear the viewport gl.enable(gl.SCISSOR_TEST); gl.scissor(viewport.x * dpr, viewport.y * dpr, viewport.width * dpr, viewport.height * dpr); } gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); gl.clear(gl.COLOR_BUFFER_BIT); gl.enable(gl.BLEND); if (viewport) { gl.disable(gl.SCISSOR_TEST); } this.trigger('startlightaccumulate', renderer, scene, camera); var viewProjectionInv = new Matrix4(); Matrix4.multiply(viewProjectionInv, camera.worldTransform, camera.invProjectionMatrix); var volumeMeshList = []; for (var i = 0; i < scene.lights.length; i++) { var light = scene.lights[i]; if (light.invisible) { continue; } var uTpl = light.uniformTemplates; var volumeMesh = light.volumeMesh || light.__volumeMesh; if (volumeMesh) { var material = volumeMesh.material; // Volume mesh will affect the scene bounding box when rendering // if castShadow is true volumeMesh.castShadow = false; var unknownLightType = false; switch (light.type) { case 'POINT_LIGHT': material.setUniform('lightColor', uTpl.pointLightColor.value(light)); material.setUniform('lightRange', uTpl.pointLightRange.value(light)); material.setUniform('lightPosition', uTpl.pointLightPosition.value(light)); break; case 'SPOT_LIGHT': material.setUniform('lightPosition', uTpl.spotLightPosition.value(light)); material.setUniform('lightColor', uTpl.spotLightColor.value(light)); material.setUniform('lightRange', uTpl.spotLightRange.value(light)); material.setUniform('lightDirection', uTpl.spotLightDirection.value(light)); material.setUniform('umbraAngleCosine', uTpl.spotLightUmbraAngleCosine.value(light)); material.setUniform('penumbraAngleCosine', uTpl.spotLightPenumbraAngleCosine.value(light)); material.setUniform('falloffFactor', uTpl.spotLightFalloffFactor.value(light)); break; case 'SPHERE_LIGHT': material.setUniform('lightColor', uTpl.sphereLightColor.value(light)); material.setUniform('lightRange', uTpl.sphereLightRange.value(light)); material.setUniform('lightRadius', uTpl.sphereLightRadius.value(light)); material.setUniform('lightPosition', uTpl.sphereLightPosition.value(light)); break; case 'TUBE_LIGHT': material.setUniform('lightColor', uTpl.tubeLightColor.value(light)); material.setUniform('lightRange', uTpl.tubeLightRange.value(light)); material.setUniform('lightExtend', uTpl.tubeLightExtend.value(light)); material.setUniform('lightPosition', uTpl.tubeLightPosition.value(light)); break; default: unknownLightType = true; } if (unknownLightType) { continue; } material.setUniform('eyePosition', eyePosition); material.setUniform('viewProjectionInv', viewProjectionInv.array); material.setUniform('gBufferTexture1', this._gBuffer.getTargetTexture1()); material.setUniform('gBufferTexture2', this._gBuffer.getTargetTexture2()); material.setUniform('gBufferTexture3', this._gBuffer.getTargetTexture3()); volumeMeshList.push(volumeMesh); } else { var pass = this._fullQuadPass; var unknownLightType = false; // Full quad light switch (light.type) { case 'AMBIENT_LIGHT': pass.material = this._ambientMat; pass.material.setUniform('lightColor', uTpl.ambientLightColor.value(light)); break; case 'AMBIENT_SH_LIGHT': pass.material = this._ambientSHMat; pass.material.setUniform('lightColor', uTpl.ambientSHLightColor.value(light)); pass.material.setUniform('lightCoefficients', uTpl.ambientSHLightCoefficients.value(light)); break; case 'AMBIENT_CUBEMAP_LIGHT': pass.material = this._ambientCubemapMat; pass.material.setUniform('lightColor', uTpl.ambientCubemapLightColor.value(light)); pass.material.setUniform('lightCubemap', uTpl.ambientCubemapLightCubemap.value(light)); pass.material.setUniform('brdfLookup', uTpl.ambientCubemapLightBRDFLookup.value(light)); break; case 'DIRECTIONAL_LIGHT': var hasShadow = shadowMapPass && light.castShadow; pass.material = this._directionalLightMat; pass.material[hasShadow ? 'define' : 'undefine']('fragment', 'SHADOWMAP_ENABLED'); if (hasShadow) { pass.material.define('fragment', 'SHADOW_CASCADE', light.shadowCascade); } pass.material.setUniform('lightColor', uTpl.directionalLightColor.value(light)); pass.material.setUniform('lightDirection', uTpl.directionalLightDirection.value(light)); break; default: // Unkonw light type unknownLightType = true; } if (unknownLightType) { continue; } var passMaterial = pass.material; passMaterial.setUniform('eyePosition', eyePosition); passMaterial.setUniform('viewProjectionInv', viewProjectionInv.array); passMaterial.setUniform('gBufferTexture1', this._gBuffer.getTargetTexture1()); passMaterial.setUniform('gBufferTexture2', this._gBuffer.getTargetTexture2()); passMaterial.setUniform('gBufferTexture3', this._gBuffer.getTargetTexture3()); // TODO if (shadowMapPass && light.castShadow) { passMaterial.setUniform('lightShadowMap', light.__shadowMap); passMaterial.setUniform('lightMatrices', light.__lightMatrices); passMaterial.setUniform('shadowCascadeClipsNear', light.__cascadeClipsNear); passMaterial.setUniform('shadowCascadeClipsFar', light.__cascadeClipsFar); passMaterial.setUniform('lightShadowMapSize', light.shadowResolution); } pass.renderQuad(renderer); } } this._renderVolumeMeshList(renderer, scene, camera, volumeMeshList); this.trigger('lightaccumulate', renderer, scene, camera); lightAccumFrameBuffer.unbind(renderer); this.trigger('afterlightaccumulate', renderer, scene, camera); }, _prepareLightShadow: (function () { var worldView = new Matrix4(); return function (renderer, scene, camera) { for (var i = 0; i < scene.lights.length; i++) { var light = scene.lights[i]; var volumeMesh = light.volumeMesh || light.__volumeMesh; if (!light.castShadow || light.invisible) { continue; } switch (light.type) { case 'POINT_LIGHT': case 'SPOT_LIGHT': // Frustum culling Matrix4.multiply(worldView, camera.viewMatrix, volumeMesh.worldTransform); if (scene.isFrustumCulled(volumeMesh, camera, worldView.array)) { continue; } this._prepareSingleLightShadow( renderer, scene, camera, light, volumeMesh.material ); break; case 'DIRECTIONAL_LIGHT': this._prepareSingleLightShadow( renderer, scene, camera, light, null ); } } }; })(), _prepareSingleLightShadow: function (renderer, scene, camera, light, material) { switch (light.type) { case 'POINT_LIGHT': var shadowMaps = []; this.shadowMapPass.renderPointLightShadow( renderer, scene, light, shadowMaps ); material.setUniform('lightShadowMap', shadowMaps[0]); material.setUniform('lightShadowMapSize', light.shadowResolution); break; case 'SPOT_LIGHT': var shadowMaps = []; var lightMatrices = []; this.shadowMapPass.renderSpotLightShadow( renderer, scene, light, lightMatrices, shadowMaps ); material.setUniform('lightShadowMap', shadowMaps[0]); material.setUniform('lightMatrix', lightMatrices[0]); material.setUniform('lightShadowMapSize', light.shadowResolution); break; case 'DIRECTIONAL_LIGHT': var shadowMaps = []; var lightMatrices = []; var cascadeClips = []; this.shadowMapPass.renderDirectionalLightShadow( renderer, scene, camera, light, cascadeClips, lightMatrices, shadowMaps ); var cascadeClipsNear = cascadeClips.slice(); var cascadeClipsFar = cascadeClips.slice(); cascadeClipsNear.pop(); cascadeClipsFar.shift(); // Iterate from far to near cascadeClipsNear.reverse(); cascadeClipsFar.reverse(); lightMatrices.reverse(); light.__cascadeClipsNear = cascadeClipsNear; light.__cascadeClipsFar = cascadeClipsFar; light.__shadowMap = shadowMaps[0]; light.__lightMatrices = lightMatrices; break; } }, // Update light volume mesh // Light volume mesh is rendered in light accumulate pass instead of full quad. // It will reduce pixels significantly when local light is relatively small. // And we can use custom volume mesh to shape the light. // // See "Deferred Shading Optimizations" in GDC2011 _updateLightProxy: function (light) { var volumeMesh; if (light.volumeMesh) { volumeMesh = light.volumeMesh; } else { switch (light.type) { // Only local light (point and spot) needs volume mesh. // Directional and ambient light renders in full quad case 'POINT_LIGHT': case 'SPHERE_LIGHT': var shader = light.type === 'SPHERE_LIGHT' ? this._sphereLightShader : this._pointLightShader; // Volume mesh created automatically if (!light.__volumeMesh) { light.__volumeMesh = new Mesh({ material: this._createLightPassMat(shader), geometry: this._lightSphereGeo, // Disable culling // if light volume mesh intersect camera near plane // We need mesh inside can still be rendered culling: false }); } volumeMesh = light.__volumeMesh; var r = light.range + (light.radius || 0); volumeMesh.scale.set(r, r, r); break; case 'SPOT_LIGHT': light.__volumeMesh = light.__volumeMesh || new Mesh({ material: this._createLightPassMat(this._spotLightShader), geometry: this._lightConeGeo, culling: false }); volumeMesh = light.__volumeMesh; var aspect = Math.tan(light.penumbraAngle * Math.PI / 180); var range = light.range; volumeMesh.scale.set(aspect * range, aspect * range, range / 2); break; case 'TUBE_LIGHT': light.__volumeMesh = light.__volumeMesh || new Mesh({ material: this._createLightPassMat(this._tubeLightShader), geometry: this._lightCylinderGeo, culling: false }); volumeMesh = light.__volumeMesh; var range = light.range; volumeMesh.scale.set(light.length / 2 + range, range, range); break; } } if (volumeMesh) { volumeMesh.update(); // Apply light transform Matrix4.multiply(volumeMesh.worldTransform, light.worldTransform, volumeMesh.worldTransform); var hasShadow = this.shadowMapPass && light.castShadow; volumeMesh.material[hasShadow ? 'define' : 'undefine']('fragment', 'SHADOWMAP_ENABLED'); } }, _renderVolumeMeshList: (function () { var worldView = new Matrix4(); var preZMaterial = new Material({ shader: new Shader(Shader.source('clay.prez.vertex'), Shader.source('clay.prez.fragment')) }); function getPreZMaterial() { return preZMaterial; } return function (renderer, scene, camera, volumeMeshList) { var gl = renderer.gl; gl.depthFunc(gl.LEQUAL); for (var i = 0; i < volumeMeshList.length; i++) { var volumeMesh = volumeMeshList[i]; // Frustum culling Matrix4.multiply(worldView, camera.viewMatrix, volumeMesh.worldTransform); if (scene.isFrustumCulled(volumeMesh, camera, worldView.array)) { continue; } // Use prez to avoid one pixel rendered twice gl.colorMask(false, false, false, false); gl.depthMask(true); // depthMask must be enabled before clear DEPTH_BUFFER gl.clear(gl.DEPTH_BUFFER_BIT); renderer.renderPass([volumeMesh], camera, { getMaterial: getPreZMaterial }); // Render light gl.colorMask(true, true, true, true); volumeMesh.material.depthMask = true; renderer.renderPass([volumeMesh], camera); } gl.depthFunc(gl.LESS); }; })(), /** * @param {clay.Renderer} renderer */ dispose: function (renderer) { this._gBuffer.dispose(renderer); this._lightAccumFrameBuffer.dispose(renderer); this._lightAccumTex.dispose(renderer); this._lightConeGeo.dispose(renderer); this._lightCylinderGeo.dispose(renderer); this._lightSphereGeo.dispose(renderer); this._fullQuadPass.dispose(renderer); this._outputPass.dispose(renderer); this._directionalLightMat.dispose(renderer); this.shadowMapPass.dispose(renderer); } }); export default DeferredRenderer;