// TODO Resources like shader, texture, geometry reference management // Trace and find out which shader, texture, geometry can be destroyed import Base from './core/Base'; import GLInfo from './core/GLInfo'; import glenum from './core/glenum'; import vendor from './core/vendor'; import Material from './Material'; import Vector2 from './math/Vector2'; import ProgramManager from './gpu/ProgramManager'; // Light header import Shader from './Shader'; import prezEssl from './shader/source/prez.glsl.js'; Shader['import'](prezEssl); import mat4 from './glmatrix/mat4'; import vec3 from './glmatrix/vec3'; var mat4Create = mat4.create; var errorShader = {}; function defaultGetMaterial(renderable) { return renderable.material; } function defaultGetUniform(renderable, material, symbol) { return material.uniforms[symbol].value; } function defaultIsMaterialChanged(renderabled, prevRenderable, material, prevMaterial) { return material !== prevMaterial; } function defaultIfRender(renderable) { return true; } function noop() {} var attributeBufferTypeMap = { float: glenum.FLOAT, byte: glenum.BYTE, ubyte: glenum.UNSIGNED_BYTE, short: glenum.SHORT, ushort: glenum.UNSIGNED_SHORT }; function VertexArrayObject(availableAttributes, availableAttributeSymbols, indicesBuffer) { this.availableAttributes = availableAttributes; this.availableAttributeSymbols = availableAttributeSymbols; this.indicesBuffer = indicesBuffer; this.vao = null; } function PlaceHolderTexture(renderer) { var blankCanvas; var webglTexture; this.bind = function (renderer) { if (!blankCanvas) { // TODO Environment not support createCanvas. blankCanvas = vendor.createCanvas(); blankCanvas.width = blankCanvas.height = 1; blankCanvas.getContext('2d'); } var gl = renderer.gl; var firstBind = !webglTexture; if (firstBind) { webglTexture = gl.createTexture(); } gl.bindTexture(gl.TEXTURE_2D, webglTexture); if (firstBind) { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, blankCanvas); } }; this.unbind = function (renderer) { renderer.gl.bindTexture(renderer.gl.TEXTURE_2D, null); }; this.isRenderable = function () { return true; }; } /** * @constructor clay.Renderer * @extends clay.core.Base */ var Renderer = Base.extend(function () { return /** @lends clay.Renderer# */ { /** * @type {HTMLCanvasElement} * @readonly */ canvas: null, /** * Canvas width, set by resize method * @type {number} * @private */ _width: 100, /** * Canvas width, set by resize method * @type {number} * @private */ _height: 100, /** * Device pixel ratio, set by setDevicePixelRatio method * Specially for high defination display * @see http://www.khronos.org/webgl/wiki/HandlingHighDPI * @type {number} * @private */ devicePixelRatio: (typeof window !== 'undefined' && window.devicePixelRatio) || 1.0, /** * Clear color * @type {number[]} */ clearColor: [0.0, 0.0, 0.0, 0.0], /** * Default: * _gl.COLOR_BUFFER_BIT | _gl.DEPTH_BUFFER_BIT | _gl.STENCIL_BUFFER_BIT * @type {number} */ clearBit: 17664, // Settings when getting context // http://www.khronos.org/registry/webgl/specs/latest/#2.4 /** * If enable alpha, default true * @type {boolean} */ alpha: true, /** * If enable depth buffer, default true * @type {boolean} */ depth: true, /** * If enable stencil buffer, default false * @type {boolean} */ stencil: false, /** * If enable antialias, default true * @type {boolean} */ antialias: true, /** * If enable premultiplied alpha, default true * @type {boolean} */ premultipliedAlpha: true, /** * If preserve drawing buffer, default false * @type {boolean} */ preserveDrawingBuffer: false, /** * If throw context error, usually turned on in debug mode * @type {boolean} */ throwError: true, /** * WebGL Context created from given canvas * @type {WebGLRenderingContext} */ gl: null, /** * Renderer viewport, read-only, can be set by setViewport method * @type {Object} */ viewport: {}, /** * Max joint number * @type {number} */ maxJointNumber: 20, // Set by FrameBuffer#bind __currentFrameBuffer: null, _viewportStack: [], _clearStack: [], _sceneRendering: null }; }, function () { if (!this.canvas) { this.canvas = vendor.createCanvas(); } var canvas = this.canvas; try { var opts = { alpha: this.alpha, depth: this.depth, stencil: this.stencil, antialias: this.antialias, premultipliedAlpha: this.premultipliedAlpha, preserveDrawingBuffer: this.preserveDrawingBuffer }; this.gl = canvas.getContext('webgl', opts) || canvas.getContext('experimental-webgl', opts); if (!this.gl) { throw new Error(); } this._glinfo = new GLInfo(this.gl); if (this.gl.targetRenderer) { console.error('Already created a renderer'); } this.gl.targetRenderer = this; this.resize(); } catch (e) { throw 'Error creating WebGL Context ' + e; } // Init managers this._programMgr = new ProgramManager(this); this._placeholderTexture = new PlaceHolderTexture(this); }, /** @lends clay.Renderer.prototype. **/ { /** * Resize the canvas * @param {number} width * @param {number} height */ resize: function(width, height) { var canvas = this.canvas; // http://www.khronos.org/webgl/wiki/HandlingHighDPI // set the display size of the canvas. var dpr = this.devicePixelRatio; if (width != null) { if (canvas.style) { canvas.style.width = width + 'px'; canvas.style.height = height + 'px'; } // set the size of the drawingBuffer canvas.width = width * dpr; canvas.height = height * dpr; this._width = width; this._height = height; } else { this._width = canvas.width / dpr; this._height = canvas.height / dpr; } this.setViewport(0, 0, this._width, this._height); }, /** * Get renderer width * @return {number} */ getWidth: function () { return this._width; }, /** * Get renderer height * @return {number} */ getHeight: function () { return this._height; }, /** * Get viewport aspect, * @return {number} */ getViewportAspect: function () { var viewport = this.viewport; return viewport.width / viewport.height; }, /** * Set devicePixelRatio * @param {number} devicePixelRatio */ setDevicePixelRatio: function(devicePixelRatio) { this.devicePixelRatio = devicePixelRatio; this.resize(this._width, this._height); }, /** * Get devicePixelRatio * @param {number} devicePixelRatio */ getDevicePixelRatio: function () { return this.devicePixelRatio; }, /** * Get WebGL extension * @param {string} name * @return {object} */ getGLExtension: function (name) { return this._glinfo.getExtension(name); }, /** * Get WebGL parameter * @param {string} name * @return {*} */ getGLParameter: function (name) { return this._glinfo.getParameter(name); }, /** * Set rendering viewport * @param {number|Object} x * @param {number} [y] * @param {number} [width] * @param {number} [height] * @param {number} [devicePixelRatio] * Defaultly use the renderere devicePixelRatio * It needs to be 1 when setViewport is called by frameBuffer * * @example * setViewport(0,0,width,height,1) * setViewport({ * x: 0, * y: 0, * width: width, * height: height, * devicePixelRatio: 1 * }) */ setViewport: function (x, y, width, height, dpr) { if (typeof x === 'object') { var obj = x; x = obj.x; y = obj.y; width = obj.width; height = obj.height; dpr = obj.devicePixelRatio; } dpr = dpr || this.devicePixelRatio; this.gl.viewport( x * dpr, y * dpr, width * dpr, height * dpr ); // Use a fresh new object, not write property. this.viewport = { x: x, y: y, width: width, height: height, devicePixelRatio: dpr }; }, /** * Push current viewport into a stack */ saveViewport: function () { this._viewportStack.push(this.viewport); }, /** * Pop viewport from stack, restore in the renderer */ restoreViewport: function () { if (this._viewportStack.length > 0) { this.setViewport(this._viewportStack.pop()); } }, /** * Push current clear into a stack */ saveClear: function () { this._clearStack.push({ clearBit: this.clearBit, clearColor: this.clearColor }); }, /** * Pop clear from stack, restore in the renderer */ restoreClear: function () { if (this._clearStack.length > 0) { var opt = this._clearStack.pop(); this.clearColor = opt.clearColor; this.clearBit = opt.clearBit; } }, bindSceneRendering: function (scene) { this._sceneRendering = scene; }, /** * Render the scene in camera to the screen or binded offline framebuffer * @param {clay.Scene} scene * @param {clay.Camera} camera * @param {boolean} [notUpdateScene] If not call the scene.update methods in the rendering, default true * @param {boolean} [preZ] If use preZ optimization, default false * @return {IRenderInfo} */ render: function(scene, camera, notUpdateScene, preZ) { var _gl = this.gl; var clearColor = this.clearColor; if (this.clearBit) { // Must set depth and color mask true before clear _gl.colorMask(true, true, true, true); _gl.depthMask(true); var viewport = this.viewport; var needsScissor = false; var viewportDpr = viewport.devicePixelRatio; if (viewport.width !== this._width || viewport.height !== this._height || (viewportDpr && viewportDpr !== this.devicePixelRatio) || viewport.x || viewport.y ) { needsScissor = true; // http://stackoverflow.com/questions/11544608/how-to-clear-a-rectangle-area-in-webgl // Only clear the viewport _gl.enable(_gl.SCISSOR_TEST); _gl.scissor(viewport.x * viewportDpr, viewport.y * viewportDpr, viewport.width * viewportDpr, viewport.height * viewportDpr); } _gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); _gl.clear(this.clearBit); if (needsScissor) { _gl.disable(_gl.SCISSOR_TEST); } } // If the scene have been updated in the prepass like shadow map // There is no need to update it again if (!notUpdateScene) { scene.update(false); } scene.updateLights(); camera = camera || scene.getMainCamera(); if (!camera) { console.error('Can\'t find camera in the scene.'); return; } camera.update(); var renderList = scene.updateRenderList(camera, true); this._sceneRendering = scene; var opaqueList = renderList.opaque; var transparentList = renderList.transparent; var sceneMaterial = scene.material; scene.trigger('beforerender', this, scene, camera, renderList); // Render pre z if (preZ) { this.renderPreZ(opaqueList, scene, camera); _gl.depthFunc(_gl.LEQUAL); } else { _gl.depthFunc(_gl.LESS); } // Update the depth of transparent list. var worldViewMat = mat4Create(); var posViewSpace = vec3.create(); for (var i = 0; i < transparentList.length; i++) { var renderable = transparentList[i]; mat4.multiplyAffine(worldViewMat, camera.viewMatrix.array, renderable.worldTransform.array); vec3.transformMat4(posViewSpace, renderable.position.array, worldViewMat); renderable.__depth = posViewSpace[2]; } // Render opaque list this.renderPass(opaqueList, camera, { getMaterial: function (renderable) { return sceneMaterial || renderable.material; }, sortCompare: this.opaqueSortCompare }); this.renderPass(transparentList, camera, { getMaterial: function (renderable) { return sceneMaterial || renderable.material; }, sortCompare: this.transparentSortCompare }); scene.trigger('afterrender', this, scene, camera, renderList); // Cleanup this._sceneRendering = null; }, getProgram: function (renderable, renderMaterial, scene) { renderMaterial = renderMaterial || renderable.material; return this._programMgr.getProgram(renderable, renderMaterial, scene); }, validateProgram: function (program) { if (program.__error) { var errorMsg = program.__error; if (errorShader[program.__uid__]) { return; } errorShader[program.__uid__] = true; if (this.throwError) { throw new Error(errorMsg); } else { this.trigger('error', errorMsg); } } }, updatePrograms: function (list, scene, passConfig) { var getMaterial = (passConfig && passConfig.getMaterial) || defaultGetMaterial; scene = scene || null; for (var i = 0; i < list.length; i++) { var renderable = list[i]; var renderMaterial = getMaterial.call(this, renderable); if (i > 0) { var prevRenderable = list[i - 1]; var prevJointsLen = prevRenderable.joints ? prevRenderable.joints.length : 0; var jointsLen = renderable.joints ? renderable.joints.length : 0; // Keep program not change if joints, material, lightGroup are same of two renderables. if (jointsLen === prevJointsLen && renderable.material === prevRenderable.material && renderable.lightGroup === prevRenderable.lightGroup ) { renderable.__program = prevRenderable.__program; continue; } } var program = this._programMgr.getProgram(renderable, renderMaterial, scene); this.validateProgram(program); renderable.__program = program; } }, /** * Render a single renderable list in camera in sequence * @param {clay.Renderable[]} list List of all renderables. * @param {clay.Camera} [camera] Camera provide view matrix and porjection matrix. It can be null. * @param {Object} [passConfig] * @param {Function} [passConfig.getMaterial] Get renderable material. * @param {Function} [passConfig.getUniform] Get material uniform value. * @param {Function} [passConfig.isMaterialChanged] If material changed. * @param {Function} [passConfig.beforeRender] Before render each renderable. * @param {Function} [passConfig.afterRender] After render each renderable * @param {Function} [passConfig.ifRender] If render the renderable. * @param {Function} [passConfig.sortCompare] Sort compare function. * @return {IRenderInfo} */ renderPass: function(list, camera, passConfig) { this.trigger('beforerenderpass', this, list, camera, passConfig); passConfig = passConfig || {}; passConfig.getMaterial = passConfig.getMaterial || defaultGetMaterial; passConfig.getUniform = passConfig.getUniform || defaultGetUniform; // PENDING Better solution? passConfig.isMaterialChanged = passConfig.isMaterialChanged || defaultIsMaterialChanged; passConfig.beforeRender = passConfig.beforeRender || noop; passConfig.afterRender = passConfig.afterRender || noop; var ifRenderObject = passConfig.ifRender || defaultIfRender; this.updatePrograms(list, this._sceneRendering, passConfig); if (passConfig.sortCompare) { list.sort(passConfig.sortCompare); } // Some common builtin uniforms var viewport = this.viewport; var vDpr = viewport.devicePixelRatio; var viewportUniform = [ viewport.x * vDpr, viewport.y * vDpr, viewport.width * vDpr, viewport.height * vDpr ]; var windowDpr = this.devicePixelRatio; var windowSizeUniform = this.__currentFrameBuffer ? [this.__currentFrameBuffer.getTextureWidth(), this.__currentFrameBuffer.getTextureHeight()] : [this._width * windowDpr, this._height * windowDpr]; // DEPRECATED var viewportSizeUniform = [ viewportUniform[2], viewportUniform[3] ]; var time = Date.now(); // Calculate view and projection matrix if (camera) { mat4.copy(matrices.VIEW, camera.viewMatrix.array); mat4.copy(matrices.PROJECTION, camera.projectionMatrix.array); mat4.copy(matrices.VIEWINVERSE, camera.worldTransform.array); } else { mat4.identity(matrices.VIEW); mat4.identity(matrices.PROJECTION); mat4.identity(matrices.VIEWINVERSE); } mat4.multiply(matrices.VIEWPROJECTION, matrices.PROJECTION, matrices.VIEW); mat4.invert(matrices.PROJECTIONINVERSE, matrices.PROJECTION); mat4.invert(matrices.VIEWPROJECTIONINVERSE, matrices.VIEWPROJECTION); var _gl = this.gl; var scene = this._sceneRendering; var prevMaterial; var prevProgram; var prevRenderable; // Status var depthTest, depthMask; var culling, cullFace, frontFace; var transparent; var drawID; var currentVAO; var materialTakesTextureSlot; // var vaoExt = this.getGLExtension('OES_vertex_array_object'); // not use vaoExt, some platforms may mess it up. var vaoExt = null; for (var i = 0; i < list.length; i++) { var renderable = list[i]; var isSceneNode = renderable.worldTransform != null; var worldM; if (!ifRenderObject(renderable)) { continue; } // Skinned mesh will transformed to joint space. Ignore the mesh transform if (isSceneNode) { worldM = (renderable.isSkinnedMesh && renderable.isSkinnedMesh()) // TODO ? (renderable.offsetMatrix ? renderable.offsetMatrix.array :matrices.IDENTITY) : renderable.worldTransform.array; } var geometry = renderable.geometry; var material = passConfig.getMaterial.call(this, renderable); var program = renderable.__program; var shader = material.shader; var currentDrawID = geometry.__uid__ + '-' + program.__uid__; var drawIDChanged = currentDrawID !== drawID; drawID = currentDrawID; if (drawIDChanged && vaoExt) { // TODO Seems need to be bound to null immediately (or before bind another program?) if vao is changed vaoExt.bindVertexArrayOES(null); } if (isSceneNode) { mat4.copy(matrices.WORLD, worldM); mat4.multiply(matrices.WORLDVIEWPROJECTION, matrices.VIEWPROJECTION, worldM); mat4.multiplyAffine(matrices.WORLDVIEW, matrices.VIEW, worldM); if (shader.matrixSemantics.WORLDINVERSE || shader.matrixSemantics.WORLDINVERSETRANSPOSE) { mat4.invert(matrices.WORLDINVERSE, worldM); } if (shader.matrixSemantics.WORLDVIEWINVERSE || shader.matrixSemantics.WORLDVIEWINVERSETRANSPOSE) { mat4.invert(matrices.WORLDVIEWINVERSE, matrices.WORLDVIEW); } if (shader.matrixSemantics.WORLDVIEWPROJECTIONINVERSE || shader.matrixSemantics.WORLDVIEWPROJECTIONINVERSETRANSPOSE) { mat4.invert(matrices.WORLDVIEWPROJECTIONINVERSE, matrices.WORLDVIEWPROJECTION); } } // Before render hook renderable.beforeRender && renderable.beforeRender(this); passConfig.beforeRender.call(this, renderable, material, prevMaterial); var programChanged = program !== prevProgram; if (programChanged) { // Set lights number program.bind(this); // Set some common uniforms program.setUniformOfSemantic(_gl, 'VIEWPORT', viewportUniform); program.setUniformOfSemantic(_gl, 'WINDOW_SIZE', windowSizeUniform); if (camera) { program.setUniformOfSemantic(_gl, 'NEAR', camera.near); program.setUniformOfSemantic(_gl, 'FAR', camera.far); } program.setUniformOfSemantic(_gl, 'DEVICEPIXELRATIO', vDpr); program.setUniformOfSemantic(_gl, 'TIME', time); // DEPRECATED program.setUniformOfSemantic(_gl, 'VIEWPORT_SIZE', viewportSizeUniform); // Set lights uniforms // TODO needs optimized if (scene) { scene.setLightUniforms(program, renderable.lightGroup, this); } } else { program = prevProgram; } // Program changes also needs reset the materials. if (programChanged || passConfig.isMaterialChanged( renderable, prevRenderable, material, prevMaterial )) { if (material.depthTest !== depthTest) { material.depthTest ? _gl.enable(_gl.DEPTH_TEST) : _gl.disable(_gl.DEPTH_TEST); depthTest = material.depthTest; } if (material.depthMask !== depthMask) { _gl.depthMask(material.depthMask); depthMask = material.depthMask; } if (material.transparent !== transparent) { material.transparent ? _gl.enable(_gl.BLEND) : _gl.disable(_gl.BLEND); transparent = material.transparent; } // TODO cache blending if (material.transparent) { if (material.blend) { material.blend(_gl); } else { // Default blend function _gl.blendEquationSeparate(_gl.FUNC_ADD, _gl.FUNC_ADD); _gl.blendFuncSeparate(_gl.SRC_ALPHA, _gl.ONE_MINUS_SRC_ALPHA, _gl.ONE, _gl.ONE_MINUS_SRC_ALPHA); } } materialTakesTextureSlot = this._bindMaterial( renderable, material, program, prevRenderable || null, prevMaterial || null, prevProgram || null, passConfig.getUniform ); prevMaterial = material; } var matrixSemanticKeys = shader.matrixSemanticKeys; if (isSceneNode) { for (var k = 0; k < matrixSemanticKeys.length; k++) { var semantic = matrixSemanticKeys[k]; var semanticInfo = shader.matrixSemantics[semantic]; var matrix = matrices[semantic]; if (semanticInfo.isTranspose) { var matrixNoTranspose = matrices[semanticInfo.semanticNoTranspose]; mat4.transpose(matrix, matrixNoTranspose); } program.setUniform(_gl, semanticInfo.type, semanticInfo.symbol, matrix); } } if (renderable.cullFace !== cullFace) { cullFace = renderable.cullFace; _gl.cullFace(cullFace); } if (renderable.frontFace !== frontFace) { frontFace = renderable.frontFace; _gl.frontFace(frontFace); } if (renderable.culling !== culling) { culling = renderable.culling; culling ? _gl.enable(_gl.CULL_FACE) : _gl.disable(_gl.CULL_FACE); } // TODO Not update skeleton in each renderable. this._updateSkeleton(renderable, program, materialTakesTextureSlot); if (drawIDChanged) { currentVAO = this._bindVAO(vaoExt, shader, geometry, program); } this._renderObject(renderable, currentVAO, program); // After render hook passConfig.afterRender(this, renderable); renderable.afterRender && renderable.afterRender(this); prevProgram = program; prevRenderable = renderable; } // TODO Seems need to be bound to null immediately if vao is changed? if (vaoExt) { vaoExt.bindVertexArrayOES(null); } this.trigger('afterrenderpass', this, list, camera, passConfig); }, getMaxJointNumber: function () { return this.maxJointNumber; }, _updateSkeleton: function (object, program, slot) { var _gl = this.gl; var skeleton = object.skeleton; // Set pose matrices of skinned mesh if (skeleton) { // TODO Update before culling. skeleton.update(); if (object.joints.length > this.getMaxJointNumber()) { var skinMatricesTexture = skeleton.getSubSkinMatricesTexture(object.__uid__, object.joints); program.useTextureSlot(this, skinMatricesTexture, slot); program.setUniform(_gl, '1i', 'skinMatricesTexture', slot); program.setUniform(_gl, '1f', 'skinMatricesTextureSize', skinMatricesTexture.width); } else { var skinMatricesArray = skeleton.getSubSkinMatrices(object.__uid__, object.joints); program.setUniformOfSemantic(_gl, 'SKIN_MATRIX', skinMatricesArray); } } }, _renderObject: function (renderable, vao, program) { var _gl = this.gl; var geometry = renderable.geometry; var glDrawMode = renderable.mode; if (glDrawMode == null) { glDrawMode = 0x0004; } var ext = null; var isInstanced = renderable.isInstancedMesh && renderable.isInstancedMesh(); if (isInstanced) { ext = this.getGLExtension('ANGLE_instanced_arrays'); if (!ext) { console.warn('Device not support ANGLE_instanced_arrays extension'); return; } } var instancedAttrLocations; if (isInstanced) { instancedAttrLocations = this._bindInstancedAttributes(renderable, program, ext); } if (vao.indicesBuffer) { var uintExt = this.getGLExtension('OES_element_index_uint'); var useUintExt = uintExt && (geometry.indices instanceof Uint32Array); var indicesType = useUintExt ? _gl.UNSIGNED_INT : _gl.UNSIGNED_SHORT; if (isInstanced) { ext.drawElementsInstancedANGLE( glDrawMode, vao.indicesBuffer.count, indicesType, 0, renderable.getInstanceCount() ); } else { _gl.drawElements(glDrawMode, vao.indicesBuffer.count, indicesType, 0); } } else { if (isInstanced) { ext.drawArraysInstancedANGLE(glDrawMode, 0, geometry.vertexCount, renderable.getInstanceCount()); } else { // FIXME Use vertex number in buffer // vertexCount may get the wrong value when geometry forget to mark dirty after update _gl.drawArrays(glDrawMode, 0, geometry.vertexCount); } } if (isInstanced) { for (var i = 0; i < instancedAttrLocations.length; i++) { _gl.disableVertexAttribArray(instancedAttrLocations[i]); } } }, _bindInstancedAttributes: function (renderable, program, ext) { var _gl = this.gl; var instancedBuffers = renderable.getInstancedAttributesBuffers(this); var locations = []; for (var i = 0; i < instancedBuffers.length; i++) { var bufferObj = instancedBuffers[i]; var location = program.getAttribLocation(_gl, bufferObj.symbol); if (location < 0) { continue; } var glType = attributeBufferTypeMap[bufferObj.type] || _gl.FLOAT;; _gl.enableVertexAttribArray(location); // TODO _gl.bindBuffer(_gl.ARRAY_BUFFER, bufferObj.buffer); _gl.vertexAttribPointer(location, bufferObj.size, glType, false, 0, 0); ext.vertexAttribDivisorANGLE(location, bufferObj.divisor); locations.push(location); } return locations; }, _bindMaterial: function (renderable, material, program, prevRenderable, prevMaterial, prevProgram, getUniformValue) { var _gl = this.gl; // PENDING Same texture in different material take different slot? // May use shader of other material if shader code are same var sameProgram = prevProgram === program; var currentTextureSlot = program.currentTextureSlot(); var enabledUniforms = material.getEnabledUniforms(); var textureUniforms = material.getTextureUniforms(); var placeholderTexture = this._placeholderTexture; for (var u = 0; u < textureUniforms.length; u++) { var symbol = textureUniforms[u]; var uniformValue = getUniformValue(renderable, material, symbol); var uniformType = material.uniforms[symbol].type; // Not use `instanceof` to determine if a value is texture in Material#bind. // Use type instead, in some case texture may be in different namespaces. // TODO Duck type validate. if (uniformType === 't' && uniformValue) { // Reset slot uniformValue.__slot = -1; } else if (uniformType === 'tv') { for (var i = 0; i < uniformValue.length; i++) { if (uniformValue[i]) { uniformValue[i].__slot = -1; } } } } placeholderTexture.__slot = -1; // Set uniforms for (var u = 0; u < enabledUniforms.length; u++) { var symbol = enabledUniforms[u]; var uniform = material.uniforms[symbol]; var uniformValue = getUniformValue(renderable, material, symbol); var uniformType = uniform.type; var isTexture = uniformType === 't'; if (isTexture) { if (!uniformValue || !uniformValue.isRenderable()) { uniformValue = placeholderTexture; } } // PENDING // When binding two materials with the same shader // Many uniforms will be be set twice even if they have the same value // So add a evaluation to see if the uniform is really needed to be set if (prevMaterial && sameProgram) { var prevUniformValue = getUniformValue(prevRenderable, prevMaterial, symbol); if (isTexture) { if (!prevUniformValue || !prevUniformValue.isRenderable()) { prevUniformValue = placeholderTexture; } } if (prevUniformValue === uniformValue) { if (isTexture) { // Still take the slot to make sure same texture in different materials have same slot. program.takeCurrentTextureSlot(this, null); } else if (uniformType === 'tv' && uniformValue) { for (var i = 0; i < uniformValue.length; i++) { program.takeCurrentTextureSlot(this, null); } } continue; } } if (uniformValue == null) { continue; } else if (isTexture) { if (uniformValue.__slot < 0) { var slot = program.currentTextureSlot(); var res = program.setUniform(_gl, '1i', symbol, slot); if (res) { // Texture uniform is enabled program.takeCurrentTextureSlot(this, uniformValue); uniformValue.__slot = slot; } } // Multiple uniform use same texture.. else { program.setUniform(_gl, '1i', symbol, uniformValue.__slot); } } else if (Array.isArray(uniformValue)) { if (uniformValue.length === 0) { continue; } // Texture Array if (uniformType === 'tv') { if (!program.hasUniform(symbol)) { continue; } var arr = []; for (var i = 0; i < uniformValue.length; i++) { var texture = uniformValue[i]; if (texture.__slot < 0) { var slot = program.currentTextureSlot(); arr.push(slot); program.takeCurrentTextureSlot(this, texture); texture.__slot = slot; } else { arr.push(texture.__slot); } } program.setUniform(_gl, '1iv', symbol, arr); } else { program.setUniform(_gl, uniform.type, symbol, uniformValue); } } else{ program.setUniform(_gl, uniform.type, symbol, uniformValue); } } var newSlot = program.currentTextureSlot(); // Texture slot maybe used out of material. program.resetTextureSlot(currentTextureSlot); return newSlot; }, _bindVAO: function (vaoExt, shader, geometry, program) { var isStatic = !geometry.dynamic; var _gl = this.gl; var vaoId = this.__uid__ + '-' + program.__uid__; var vao = geometry.__vaoCache[vaoId]; if (!vao) { var chunks = geometry.getBufferChunks(this); if (!chunks || !chunks.length) { // Empty mesh return; } var chunk = chunks[0]; var attributeBuffers = chunk.attributeBuffers; var indicesBuffer = chunk.indicesBuffer; var availableAttributes = []; var availableAttributeSymbols = []; for (var a = 0; a < attributeBuffers.length; a++) { var attributeBufferInfo = attributeBuffers[a]; var name = attributeBufferInfo.name; var semantic = attributeBufferInfo.semantic; var symbol; if (semantic) { var semanticInfo = shader.attributeSemantics[semantic]; symbol = semanticInfo && semanticInfo.symbol; } else { symbol = name; } if (symbol && program.attributes[symbol]) { availableAttributes.push(attributeBufferInfo); availableAttributeSymbols.push(symbol); } } vao = new VertexArrayObject( availableAttributes, availableAttributeSymbols, indicesBuffer ); if (isStatic) { geometry.__vaoCache[vaoId] = vao; } } var needsBindAttributes = true; // Create vertex object array cost a lot // So we don't use it on the dynamic object if (vaoExt && isStatic) { // Use vertex array object // http://blog.tojicode.com/2012/10/oesvertexarrayobject-extension.html if (vao.vao == null) { vao.vao = vaoExt.createVertexArrayOES(); } else { needsBindAttributes = false; } vaoExt.bindVertexArrayOES(vao.vao); } var availableAttributes = vao.availableAttributes; var indicesBuffer = vao.indicesBuffer; if (needsBindAttributes) { var locationList = program.enableAttributes(this, vao.availableAttributeSymbols, (vaoExt && isStatic && vao)); // Setting attributes; for (var a = 0; a < availableAttributes.length; a++) { var location = locationList[a]; if (location === -1) { continue; } var attributeBufferInfo = availableAttributes[a]; var buffer = attributeBufferInfo.buffer; var size = attributeBufferInfo.size; var glType = attributeBufferTypeMap[attributeBufferInfo.type] || _gl.FLOAT; _gl.bindBuffer(_gl.ARRAY_BUFFER, buffer); _gl.vertexAttribPointer(location, size, glType, false, 0, 0); } if (geometry.isUseIndices()) { _gl.bindBuffer(_gl.ELEMENT_ARRAY_BUFFER, indicesBuffer.buffer); } } return vao; }, renderPreZ: function (list, scene, camera) { var _gl = this.gl; var preZPassMaterial = this._prezMaterial || new Material({ shader: new Shader(Shader.source('clay.prez.vertex'), Shader.source('clay.prez.fragment')) }); this._prezMaterial = preZPassMaterial; _gl.colorMask(false, false, false, false); _gl.depthMask(true); // Status this.renderPass(list, camera, { ifRender: function (renderable) { return !renderable.ignorePreZ; }, isMaterialChanged: function (renderable, prevRenderable) { var matA = renderable.material; var matB = prevRenderable.material; return matA.get('diffuseMap') !== matB.get('diffuseMap') || (matA.get('alphaCutoff') || 0) !== (matB.get('alphaCutoff') || 0); }, getUniform: function (renderable, depthMaterial, symbol) { if (symbol === 'alphaMap') { return renderable.material.get('diffuseMap'); } else if (symbol === 'alphaCutoff') { if (renderable.material.isDefined('fragment', 'ALPHA_TEST') && renderable.material.get('diffuseMap') ) { var alphaCutoff = renderable.material.get('alphaCutoff'); return alphaCutoff || 0; } return 0; } else if (symbol === 'uvRepeat') { return renderable.material.get('uvRepeat'); } else if (symbol === 'uvOffset') { return renderable.material.get('uvOffset'); } else { return depthMaterial.get(symbol); } }, getMaterial: function () { return preZPassMaterial; }, sort: this.opaqueSortCompare }); _gl.colorMask(true, true, true, true); _gl.depthMask(true); }, /** * Dispose given scene, including all geometris, textures and shaders in the scene * @param {clay.Scene} scene */ disposeScene: function(scene) { this.disposeNode(scene, true, true); scene.dispose(); }, /** * Dispose given node, including all geometries, textures and shaders attached on it or its descendant * @param {clay.Node} node * @param {boolean} [disposeGeometry=false] If dispose the geometries used in the descendant mesh * @param {boolean} [disposeTexture=false] If dispose the textures used in the descendant mesh */ disposeNode: function(root, disposeGeometry, disposeTexture) { // Dettached from parent if (root.getParent()) { root.getParent().remove(root); } var disposedMap = {}; root.traverse(function(node) { var material = node.material; if (node.geometry && disposeGeometry) { node.geometry.dispose(this); } if (disposeTexture && material && !disposedMap[material.__uid__]) { var textureUniforms = material.getTextureUniforms(); for (var u = 0; u < textureUniforms.length; u++) { var uniformName = textureUniforms[u]; var val = material.uniforms[uniformName].value; var uniformType = material.uniforms[uniformName].type; if (!val) { continue; } if (uniformType === 't') { val.dispose && val.dispose(this); } else if (uniformType === 'tv') { for (var k = 0; k < val.length; k++) { if (val[k]) { val[k].dispose && val[k].dispose(this); } } } } disposedMap[material.__uid__] = true; } // Particle system and AmbientCubemap light need to dispose if (node.dispose) { node.dispose(this); } }, this); }, /** * Dispose given geometry * @param {clay.Geometry} geometry */ disposeGeometry: function(geometry) { geometry.dispose(this); }, /** * Dispose given texture * @param {clay.Texture} texture */ disposeTexture: function(texture) { texture.dispose(this); }, /** * Dispose given frame buffer * @param {clay.FrameBuffer} frameBuffer */ disposeFrameBuffer: function(frameBuffer) { frameBuffer.dispose(this); }, /** * Dispose renderer */ dispose: function () {}, /** * Convert screen coords to normalized device coordinates(NDC) * Screen coords can get from mouse event, it is positioned relative to canvas element * NDC can be used in ray casting with Camera.prototype.castRay methods * * @param {number} x * @param {number} y * @param {clay.Vector2} [out] * @return {clay.Vector2} */ screenToNDC: function(x, y, out) { if (!out) { out = new Vector2(); } // Invert y; y = this._height - y; var viewport = this.viewport; var arr = out.array; arr[0] = (x - viewport.x) / viewport.width; arr[0] = arr[0] * 2 - 1; arr[1] = (y - viewport.y) / viewport.height; arr[1] = arr[1] * 2 - 1; return out; } }); /** * Opaque renderables compare function * @param {clay.Renderable} x * @param {clay.Renderable} y * @return {boolean} * @static */ Renderer.opaqueSortCompare = Renderer.prototype.opaqueSortCompare = function(x, y) { // Priority renderOrder -> program -> material -> geometry if (x.renderOrder === y.renderOrder) { if (x.__program === y.__program) { if (x.material === y.material) { return x.geometry.__uid__ - y.geometry.__uid__; } return x.material.__uid__ - y.material.__uid__; } if (x.__program && y.__program) { return x.__program.__uid__ - y.__program.__uid__; } return 0; } return x.renderOrder - y.renderOrder; }; /** * Transparent renderables compare function * @param {clay.Renderable} a * @param {clay.Renderable} b * @return {boolean} * @static */ Renderer.transparentSortCompare = Renderer.prototype.transparentSortCompare = function(x, y) { // Priority renderOrder -> depth -> program -> material -> geometry if (x.renderOrder === y.renderOrder) { if (x.__depth === y.__depth) { if (x.__program === y.__program) { if (x.material === y.material) { return x.geometry.__uid__ - y.geometry.__uid__; } return x.material.__uid__ - y.material.__uid__; } if (x.__program && y.__program) { return x.__program.__uid__ - y.__program.__uid__; } return 0; } // Depth is negative // So farther object has smaller depth value return x.__depth - y.__depth; } return x.renderOrder - y.renderOrder; }; // Temporary variables var matrices = { IDENTITY: mat4Create(), WORLD: mat4Create(), VIEW: mat4Create(), PROJECTION: mat4Create(), WORLDVIEW: mat4Create(), VIEWPROJECTION: mat4Create(), WORLDVIEWPROJECTION: mat4Create(), WORLDINVERSE: mat4Create(), VIEWINVERSE: mat4Create(), PROJECTIONINVERSE: mat4Create(), WORLDVIEWINVERSE: mat4Create(), VIEWPROJECTIONINVERSE: mat4Create(), WORLDVIEWPROJECTIONINVERSE: mat4Create(), WORLDTRANSPOSE: mat4Create(), VIEWTRANSPOSE: mat4Create(), PROJECTIONTRANSPOSE: mat4Create(), WORLDVIEWTRANSPOSE: mat4Create(), VIEWPROJECTIONTRANSPOSE: mat4Create(), WORLDVIEWPROJECTIONTRANSPOSE: mat4Create(), WORLDINVERSETRANSPOSE: mat4Create(), VIEWINVERSETRANSPOSE: mat4Create(), PROJECTIONINVERSETRANSPOSE: mat4Create(), WORLDVIEWINVERSETRANSPOSE: mat4Create(), VIEWPROJECTIONINVERSETRANSPOSE: mat4Create(), WORLDVIEWPROJECTIONINVERSETRANSPOSE: mat4Create() }; /** * @name clay.Renderer.COLOR_BUFFER_BIT * @type {number} */ Renderer.COLOR_BUFFER_BIT = glenum.COLOR_BUFFER_BIT; /** * @name clay.Renderer.DEPTH_BUFFER_BIT * @type {number} */ Renderer.DEPTH_BUFFER_BIT = glenum.DEPTH_BUFFER_BIT; /** * @name clay.Renderer.STENCIL_BUFFER_BIT * @type {number} */ Renderer.STENCIL_BUFFER_BIT = glenum.STENCIL_BUFFER_BIT; export default Renderer;