import Base from '../core/Base'; import Texture2D from '../Texture2D'; import Texture from '../Texture'; import Material from '../Material'; import FrameBuffer from '../FrameBuffer'; import Shader from '../Shader'; import Pass from '../compositor/Pass'; import Matrix4 from '../math/Matrix4'; import mat4 from '../glmatrix/mat4'; import gbufferEssl from '../shader/source/deferred/gbuffer.glsl.js'; import chunkEssl from '../shader/source/deferred/chunk.glsl.js'; Shader.import(gbufferEssl); Shader.import(chunkEssl); function createFillCanvas(color) { var canvas = document.createElement('canvas'); canvas.width = canvas.height = 1; var ctx = canvas.getContext('2d'); ctx.fillStyle = color || '#000'; ctx.fillRect(0, 0, 1, 1); return canvas; } // TODO specularColor // TODO Performance improvement function getGetUniformHook1(defaultNormalMap, defaultRoughnessMap, defaultDiffuseMap) { return function (renderable, gBufferMat, symbol) { var standardMaterial = renderable.material; if (symbol === 'doubleSided') { return standardMaterial.isDefined('fragment', 'DOUBLE_SIDED'); } else if (symbol === 'uvRepeat' || symbol === 'uvOffset' || symbol === 'alpha') { return standardMaterial.get(symbol); } else if (symbol === 'normalMap') { return standardMaterial.get(symbol) || defaultNormalMap; } else if (symbol === 'diffuseMap') { return standardMaterial.get(symbol) || defaultDiffuseMap; } else if (symbol === 'alphaCutoff') { // TODO DIFFUSEMAP_ALPHA_ALPHA if (standardMaterial.isDefined('fragment', 'ALPHA_TEST')) { var alphaCutoff = standardMaterial.get('alphaCutoff'); return alphaCutoff || 0; } return 0; } else { var useRoughnessWorkflow = standardMaterial.isDefined('fragment', 'USE_ROUGHNESS'); var roughGlossMap = useRoughnessWorkflow ? standardMaterial.get('roughnessMap') : standardMaterial.get('glossinessMap'); switch (symbol) { case 'glossiness': return useRoughnessWorkflow ? (1.0 - standardMaterial.get('roughness')) : standardMaterial.get('glossiness'); case 'roughGlossMap': return roughGlossMap; case 'useRoughGlossMap': return !!roughGlossMap; case 'useRoughness': return useRoughnessWorkflow; case 'roughGlossChannel': return useRoughnessWorkflow ? standardMaterial.getDefine('fragment', 'ROUGHNESS_CHANNEL') : standardMaterial.getDefine('fragment', 'GLOSSINESS_CHANNEL'); } } }; } function getGetUniformHook2(defaultDiffuseMap, defaultMetalnessMap) { return function (renderable, gBufferMat, symbol) { var standardMaterial = renderable.material; switch (symbol) { case 'color': case 'uvRepeat': case 'uvOffset': case 'alpha': return standardMaterial.get(symbol); case 'metalness': return standardMaterial.get('metalness') || 0; case 'diffuseMap': return standardMaterial.get(symbol) || defaultDiffuseMap; case 'metalnessMap': return standardMaterial.get(symbol) || defaultMetalnessMap; case 'useMetalnessMap': return !!standardMaterial.get('metalnessMap'); case 'linear': return standardMaterial.isDefined('SRGB_DECODE'); case 'alphaCutoff': // TODO DIFFUSEMAP_ALPHA_ALPHA if (standardMaterial.isDefined('fragment', 'ALPHA_TEST')) { var alphaCutoff = standardMaterial.get('alphaCutoff'); return alphaCutoff || 0.0; } return 0.0; } }; } /** * GBuffer is provided for deferred rendering and SSAO, SSR pass. * It will do three passes rendering to four target textures. See * + {@link clay.deferred.GBuffer#getTargetTexture1} * + {@link clay.deferred.GBuffer#getTargetTexture2} * + {@link clay.deferred.GBuffer#getTargetTexture3} * + {@link clay.deferred.GBuffer#getTargetTexture4} * @constructor * @alias clay.deferred.GBuffer * @extends clay.core.Base */ var GBuffer = Base.extend(function () { var commonTextureOpts = { minFilter: Texture.NEAREST, magFilter: Texture.NEAREST, wrapS: Texture.CLAMP_TO_EDGE, wrapT: Texture.CLAMP_TO_EDGE, }; return /** @lends clay.deferred.GBuffer# */ { /** * If enable gbuffer texture 1. * @type {boolean} */ enableTargetTexture1: true, /** * If enable gbuffer texture 2. * @type {boolean} */ enableTargetTexture2: true, /** * If enable gbuffer texture 3. * @type {boolean} */ enableTargetTexture3: true, /** * If enable gbuffer texture 4. * @type {boolean} */ enableTargetTexture4: false, renderTransparent: false, _gBufferRenderList: [], // - R: normal.x // - G: normal.y // - B: normal.z // - A: glossiness _gBufferTex1: new Texture2D(Object.assign({ // PENDING type: Texture.HALF_FLOAT }, commonTextureOpts)), // - R: depth _gBufferTex2: new Texture2D(Object.assign({ // format: Texture.DEPTH_COMPONENT, // type: Texture.UNSIGNED_INT format: Texture.DEPTH_STENCIL, type: Texture.UNSIGNED_INT_24_8_WEBGL }, commonTextureOpts)), // - R: albedo.r // - G: albedo.g // - B: albedo.b // - A: metalness _gBufferTex3: new Texture2D(commonTextureOpts), _gBufferTex4: new Texture2D(Object.assign({ // FLOAT Texture has bug on iOS. is HALF_FLOAT enough? type: Texture.HALF_FLOAT }, commonTextureOpts)), _defaultNormalMap: new Texture2D({ image: createFillCanvas('#000') }), _defaultRoughnessMap: new Texture2D({ image: createFillCanvas('#fff') }), _defaultMetalnessMap: new Texture2D({ image: createFillCanvas('#fff') }), _defaultDiffuseMap: new Texture2D({ image: createFillCanvas('#fff') }), _frameBuffer: new FrameBuffer(), _gBufferMaterial1: new Material({ shader: new Shader( Shader.source('clay.deferred.gbuffer.vertex'), Shader.source('clay.deferred.gbuffer1.fragment') ), vertexDefines: { FIRST_PASS: null }, fragmentDefines: { FIRST_PASS: null } }), _gBufferMaterial2: new Material({ shader: new Shader( Shader.source('clay.deferred.gbuffer.vertex'), Shader.source('clay.deferred.gbuffer2.fragment') ), vertexDefines: { SECOND_PASS: null }, fragmentDefines: { SECOND_PASS: null } }), _gBufferMaterial3: new Material({ shader: new Shader( Shader.source('clay.deferred.gbuffer.vertex'), Shader.source('clay.deferred.gbuffer3.fragment') ), vertexDefines: { THIRD_PASS: null }, fragmentDefines: { THIRD_PASS: null } }), _debugPass: new Pass({ fragment: Shader.source('clay.deferred.gbuffer.debug') }) }; }, /** @lends clay.deferred.GBuffer# */{ /** * Set G Buffer size. * @param {number} width * @param {number} height */ resize: function (width, height) { if (this._gBufferTex1.width === width && this._gBufferTex1.height === height ) { return; } this._gBufferTex1.width = width; this._gBufferTex1.height = height; this._gBufferTex2.width = width; this._gBufferTex2.height = height; this._gBufferTex3.width = width; this._gBufferTex3.height = height; this._gBufferTex4.width = width; this._gBufferTex4.height = height; }, // TODO is dpr needed? setViewport: function (x, y, width, height, dpr) { var viewport; if (typeof x === 'object') { viewport = x; } else { viewport = { x: x, y: y, width: width, height: height, devicePixelRatio: dpr || 1 }; } this._frameBuffer.viewport = viewport; }, getViewport: function () { if (this._frameBuffer.viewport) { return this._frameBuffer.viewport; } else { return { x: 0, y: 0, width: this._gBufferTex1.width, height: this._gBufferTex1.height, devicePixelRatio: 1 }; } }, /** * Update GBuffer * @param {clay.Renderer} renderer * @param {clay.Scene} scene * @param {clay.Camera} camera * @param {Object} opts */ update: function (renderer, scene, camera, opts) { opts = opts || {}; var gl = renderer.gl; var frameBuffer = this._frameBuffer; var viewport = frameBuffer.viewport; var renderList = scene.updateRenderList(camera, true); var opaqueList = renderList.opaque; var transparentList = renderList.transparent; var offset = 0; var gBufferRenderList = this._gBufferRenderList; for (var i = 0; i < opaqueList.length; i++) { if (!opaqueList[i].ignoreGBuffer) { gBufferRenderList[offset++] = opaqueList[i]; } } if (this.renderTransparent) { for (var i = 0; i < transparentList.length; i++) { if (!transparentList[i].ignoreGBuffer) { gBufferRenderList[offset++] = transparentList[i]; } } } gBufferRenderList.length = offset; gl.clearColor(0, 0, 0, 0); gl.depthMask(true); gl.colorMask(true, true, true, true); gl.disable(gl.BLEND); var enableTargetTexture1 = this.enableTargetTexture1; var enableTargetTexture2 = this.enableTargetTexture2; var enableTargetTexture3 = this.enableTargetTexture3; var enableTargetTexture4 = this.enableTargetTexture4; if (!enableTargetTexture1 && !enableTargetTexture3 && !enableTargetTexture4) { console.warn('Can\'t disable targetTexture1, targetTexture3, targetTexture4 both'); enableTargetTexture1 = true; } if (enableTargetTexture2) { frameBuffer.attach(opts.targetTexture2 || this._gBufferTex2, renderer.gl.DEPTH_STENCIL_ATTACHMENT); } function clearViewport() { 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.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); if (viewport) { gl.disable(gl.SCISSOR_TEST); } } function isMaterialChanged(renderable, prevRenderable) { return renderable.material !== prevRenderable.material; } // PENDING, scene.boundingBoxLastFrame needs be updated if have shadow renderer.bindSceneRendering(scene); if (enableTargetTexture1) { // Pass 1 frameBuffer.attach(opts.targetTexture1 || this._gBufferTex1); frameBuffer.bind(renderer); clearViewport(); var gBufferMaterial1 = this._gBufferMaterial1; var passConfig = { getMaterial: function () { return gBufferMaterial1; }, getUniform: getGetUniformHook1(this._defaultNormalMap, this._defaultRoughnessMap, this._defaultDiffuseMap), isMaterialChanged: isMaterialChanged, sortCompare: renderer.opaqueSortCompare }; // FIXME Use MRT if possible renderer.renderPass(gBufferRenderList, camera, passConfig); } if (enableTargetTexture3) { // Pass 2 frameBuffer.attach(opts.targetTexture3 || this._gBufferTex3); frameBuffer.bind(renderer); clearViewport(); var gBufferMaterial2 = this._gBufferMaterial2; var passConfig = { getMaterial: function () { return gBufferMaterial2; }, getUniform: getGetUniformHook2(this._defaultDiffuseMap, this._defaultMetalnessMap), isMaterialChanged: isMaterialChanged, sortCompare: renderer.opaqueSortCompare }; renderer.renderPass(gBufferRenderList, camera, passConfig); } if (enableTargetTexture4) { frameBuffer.bind(renderer); frameBuffer.attach(opts.targetTexture4 || this._gBufferTex4); clearViewport(); // Remove jittering in temporal aa. // PENDING. Better solution? camera.update(); var gBufferMaterial3 = this._gBufferMaterial3; var cameraViewProj = mat4.create(); mat4.multiply(cameraViewProj, camera.projectionMatrix.array, camera.viewMatrix.array); var passConfig = { getMaterial: function () { return gBufferMaterial3; }, afterRender: function (renderer, renderable) { var isSkinnedMesh = renderable.isSkinnedMesh(); if (isSkinnedMesh) { var skeleton = renderable.skeleton; var joints = renderable.joints; if (joints.length > renderer.getMaxJointNumber()) { var skinMatricesTexture = skeleton.getSubSkinMatricesTexture(renderable.__uid__, joints); var prevSkinMatricesTexture = renderable.__prevSkinMatricesTexture; if (!prevSkinMatricesTexture) { prevSkinMatricesTexture = renderable.__prevSkinMatricesTexture = new Texture2D({ type: Texture.FLOAT, minFilter: Texture.NEAREST, magFilter: Texture.NEAREST, useMipmap: false, flipY: false }); } if (!prevSkinMatricesTexture.pixels || prevSkinMatricesTexture.pixels.length !== skinMatricesTexture.pixels.length ) { prevSkinMatricesTexture.pixels = new Float32Array(skinMatricesTexture.pixels); } else { for (var i = 0; i < skinMatricesTexture.pixels.length; i++) { prevSkinMatricesTexture.pixels[i] = skinMatricesTexture.pixels[i]; } } prevSkinMatricesTexture.width = skinMatricesTexture.width; prevSkinMatricesTexture.height = skinMatricesTexture.height; } else { var skinMatricesArray = skeleton.getSubSkinMatrices(renderable.__uid__, joints); if (!renderable.__prevSkinMatricesArray || renderable.__prevSkinMatricesArray.length !== skinMatricesArray.length) { renderable.__prevSkinMatricesArray = new Float32Array(skinMatricesArray.length); } renderable.__prevSkinMatricesArray.set(skinMatricesArray); } } renderable.__prevWorldViewProjection = renderable.__prevWorldViewProjection || mat4.create(); if (isSkinnedMesh) { // Ignore world transform of skinned mesh. mat4.copy(renderable.__prevWorldViewProjection, cameraViewProj); } else { mat4.multiply(renderable.__prevWorldViewProjection, cameraViewProj, renderable.worldTransform.array); } }, getUniform: function (renderable, gBufferMat, symbol) { if (symbol === 'prevWorldViewProjection') { return renderable.__prevWorldViewProjection; } else if (symbol === 'prevSkinMatrix') { return renderable.__prevSkinMatricesArray; } else if (symbol === 'prevSkinMatricesTexture') { return renderable.__prevSkinMatricesTexture; } else if (symbol === 'firstRender') { return !renderable.__prevWorldViewProjection; } else { return gBufferMat.get(symbol); } }, isMaterialChanged: function () { // Always update prevWorldViewProjection return true; }, sortCompare: renderer.opaqueSortCompare }; renderer.renderPass(gBufferRenderList, camera, passConfig); } renderer.bindSceneRendering(null); frameBuffer.unbind(renderer); }, /** * Debug output of gBuffer. Use `type` parameter to choos the debug output type, which can be: * * + 'normal' * + 'depth' * + 'position' * + 'glossiness' * + 'metalness' * + 'albedo' * + 'velocity' * * @param {clay.Renderer} renderer * @param {clay.Camera} camera * @param {string} [type='normal'] */ renderDebug: function (renderer, camera, type, viewport) { var debugTypes = { normal: 0, depth: 1, position: 2, glossiness: 3, metalness: 4, albedo: 5, velocity: 6 }; if (debugTypes[type] == null) { console.warn('Unkown type "' + type + '"'); // Default use normal type = 'normal'; } renderer.saveClear(); renderer.saveViewport(); renderer.clearBit = renderer.gl.DEPTH_BUFFER_BIT; if (viewport) { renderer.setViewport(viewport); } var viewProjectionInv = new Matrix4(); Matrix4.multiply(viewProjectionInv, camera.worldTransform, camera.invProjectionMatrix); var debugPass = this._debugPass; debugPass.setUniform('viewportSize', [renderer.getWidth(), renderer.getHeight()]); debugPass.setUniform('gBufferTexture1', this._gBufferTex1); debugPass.setUniform('gBufferTexture2', this._gBufferTex2); debugPass.setUniform('gBufferTexture3', this._gBufferTex3); debugPass.setUniform('gBufferTexture4', this._gBufferTex4); debugPass.setUniform('debug', debugTypes[type]); debugPass.setUniform('viewProjectionInv', viewProjectionInv.array); debugPass.render(renderer); renderer.restoreViewport(); renderer.restoreClear(); }, /** * Get first target texture. * Channel storage: * + R: normal.x * 0.5 + 0.5 * + G: normal.y * 0.5 + 0.5 * + B: normal.z * 0.5 + 0.5 * + A: glossiness * @return {clay.Texture2D} */ getTargetTexture1: function () { return this._gBufferTex1; }, /** * Get second target texture. * Channel storage: * + R: depth * @return {clay.Texture2D} */ getTargetTexture2: function () { return this._gBufferTex2; }, /** * Get third target texture. * Channel storage: * + R: albedo.r * + G: albedo.g * + B: albedo.b * + A: metalness * @return {clay.Texture2D} */ getTargetTexture3: function () { return this._gBufferTex3; }, /** * Get fourth target texture. * Channel storage: * + R: velocity.r * + G: velocity.g * @return {clay.Texture2D} */ getTargetTexture4: function () { return this._gBufferTex4; }, /** * @param {clay.Renderer} renderer */ dispose: function (renderer) { this._gBufferTex1.dispose(renderer); this._gBufferTex2.dispose(renderer); this._gBufferTex3.dispose(renderer); this._defaultNormalMap.dispose(renderer); this._defaultRoughnessMap.dispose(renderer); this._defaultMetalnessMap.dispose(renderer); this._defaultDiffuseMap.dispose(renderer); this._frameBuffer.dispose(renderer); } }); export default GBuffer;