| // Copyright 2018 The Immersive Web Community Group |
| // |
| // Permission is hereby granted, free of charge, to any person obtaining a copy |
| // of this software and associated documentation files (the "Software"), to deal |
| // in the Software without restriction, including without limitation the rights |
| // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| // copies of the Software, and to permit persons to whom the Software is |
| // furnished to do so, subject to the following conditions: |
| |
| // The above copyright notice and this permission notice shall be included in |
| // all copies or substantial portions of the Software. |
| |
| // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| // SOFTWARE. |
| |
| import {CAP, MAT_STATE, RENDER_ORDER, stateToBlendFunc} from './material.js'; |
| import {Node} from './node.js'; |
| import {Program} from './program.js'; |
| import {DataTexture, VideoTexture} from './texture.js'; |
| import {mat4, vec3} from '../math/gl-matrix.js'; |
| |
| export const ATTRIB = { |
| POSITION: 1, |
| NORMAL: 2, |
| TANGENT: 3, |
| TEXCOORD_0: 4, |
| TEXCOORD_1: 5, |
| COLOR_0: 6, |
| }; |
| |
| export const ATTRIB_MASK = { |
| POSITION: 0x0001, |
| NORMAL: 0x0002, |
| TANGENT: 0x0004, |
| TEXCOORD_0: 0x0008, |
| TEXCOORD_1: 0x0010, |
| COLOR_0: 0x0020, |
| }; |
| |
| const GL = WebGLRenderingContext; // For enums |
| |
| const DEF_LIGHT_DIR = new Float32Array([-0.1, -1.0, -0.2]); |
| const DEF_LIGHT_COLOR = new Float32Array([3.0, 3.0, 3.0]); |
| |
| const PRECISION_REGEX = new RegExp('precision (lowp|mediump|highp) float;'); |
| |
| const VERTEX_SHADER_SINGLE_ENTRY = ` |
| uniform mat4 PROJECTION_MATRIX, VIEW_MATRIX, MODEL_MATRIX; |
| |
| void main() { |
| gl_Position = vertex_main(PROJECTION_MATRIX, VIEW_MATRIX, MODEL_MATRIX); |
| } |
| `; |
| |
| const VERTEX_SHADER_MULTI_ENTRY = ` |
| #ERROR Multiview rendering is not implemented |
| void main() { |
| gl_Position = vec4(0.0, 0.0, 0.0, 1.0); |
| } |
| `; |
| |
| const FRAGMENT_SHADER_ENTRY = ` |
| void main() { |
| gl_FragColor = fragment_main(); |
| } |
| `; |
| |
| function isPowerOfTwo(n) { |
| return (n & (n - 1)) === 0; |
| } |
| |
| // Creates a WebGL context and initializes it with some common default state. |
| export function createWebGLContext(glAttribs) { |
| glAttribs = glAttribs || {alpha: false}; |
| |
| let webglCanvas = document.createElement('canvas'); |
| let contextTypes = glAttribs.webgl2 ? ['webgl2'] : ['webgl', 'experimental-webgl']; |
| let context = null; |
| |
| for (let contextType of contextTypes) { |
| context = webglCanvas.getContext(contextType, glAttribs); |
| if (context) { |
| break; |
| } |
| } |
| |
| if (!context) { |
| let webglType = (glAttribs.webgl2 ? 'WebGL 2' : 'WebGL'); |
| console.error('This browser does not support ' + webglType + '.'); |
| return null; |
| } |
| |
| return context; |
| } |
| |
| export class RenderView { |
| constructor(projectionMatrix, viewMatrix, viewport = null, eye = 'left') { |
| this.projectionMatrix = projectionMatrix; |
| this.viewMatrix = viewMatrix; |
| this.viewport = viewport; |
| // If an eye isn't given the left eye is assumed. |
| this._eye = eye; |
| this._eyeIndex = (eye == 'left' ? 0 : 1); |
| } |
| |
| get eye() { |
| return this._eye; |
| } |
| |
| set eye(value) { |
| this._eye = value; |
| this._eyeIndex = (value == 'left' ? 0 : 1); |
| } |
| |
| get eyeIndex() { |
| return this._eyeIndex; |
| } |
| } |
| |
| class RenderBuffer { |
| constructor(target, usage, buffer, length = 0) { |
| this._target = target; |
| this._usage = usage; |
| this._length = length; |
| if (buffer instanceof Promise) { |
| this._buffer = null; |
| this._promise = buffer.then((buffer) => { |
| this._buffer = buffer; |
| return this; |
| }); |
| } else { |
| this._buffer = buffer; |
| this._promise = Promise.resolve(this); |
| } |
| } |
| |
| waitForComplete() { |
| return this._promise; |
| } |
| } |
| |
| class RenderPrimitiveAttribute { |
| constructor(primitiveAttribute) { |
| this._attrib_index = ATTRIB[primitiveAttribute.name]; |
| this._componentCount = primitiveAttribute.componentCount; |
| this._componentType = primitiveAttribute.componentType; |
| this._stride = primitiveAttribute.stride; |
| this._byteOffset = primitiveAttribute.byteOffset; |
| this._normalized = primitiveAttribute.normalized; |
| } |
| } |
| |
| class RenderPrimitiveAttributeBuffer { |
| constructor(buffer) { |
| this._buffer = buffer; |
| this._attributes = []; |
| } |
| } |
| |
| class RenderPrimitive { |
| constructor(primitive) { |
| this._activeFrameId = 0; |
| this._instances = []; |
| this._material = null; |
| |
| this.setPrimitive(primitive); |
| } |
| |
| setPrimitive(primitive) { |
| this._mode = primitive.mode; |
| this._elementCount = primitive.elementCount; |
| this._promise = null; |
| this._vao = null; |
| this._complete = false; |
| this._attributeBuffers = []; |
| this._attributeMask = 0; |
| |
| for (let attribute of primitive.attributes) { |
| this._attributeMask |= ATTRIB_MASK[attribute.name]; |
| let renderAttribute = new RenderPrimitiveAttribute(attribute); |
| let foundBuffer = false; |
| for (let attributeBuffer of this._attributeBuffers) { |
| if (attributeBuffer._buffer == attribute.buffer) { |
| attributeBuffer._attributes.push(renderAttribute); |
| foundBuffer = true; |
| break; |
| } |
| } |
| if (!foundBuffer) { |
| let attributeBuffer = new RenderPrimitiveAttributeBuffer(attribute.buffer); |
| attributeBuffer._attributes.push(renderAttribute); |
| this._attributeBuffers.push(attributeBuffer); |
| } |
| } |
| |
| this._indexBuffer = null; |
| this._indexByteOffset = 0; |
| this._indexType = 0; |
| |
| if (primitive.indexBuffer) { |
| this._indexByteOffset = primitive.indexByteOffset; |
| this._indexType = primitive.indexType; |
| this._indexBuffer = primitive.indexBuffer; |
| } |
| |
| if (primitive._min) { |
| this._min = vec3.clone(primitive._min); |
| this._max = vec3.clone(primitive._max); |
| } else { |
| this._min = null; |
| this._max = null; |
| } |
| |
| if (this._material != null) { |
| this.waitForComplete(); // To flip the _complete flag. |
| } |
| } |
| |
| setRenderMaterial(material) { |
| this._material = material; |
| this._promise = null; |
| this._complete = false; |
| |
| if (this._material != null) { |
| this.waitForComplete(); // To flip the _complete flag. |
| } |
| } |
| |
| markActive(frameId) { |
| if (this._complete && this._activeFrameId != frameId) { |
| if (this._material) { |
| if (!this._material.markActive(frameId)) { |
| return; |
| } |
| } |
| this._activeFrameId = frameId; |
| } |
| } |
| |
| get samplers() { |
| return this._material._samplerDictionary; |
| } |
| |
| get uniforms() { |
| return this._material._uniform_dictionary; |
| } |
| |
| waitForComplete() { |
| if (!this._promise) { |
| if (!this._material) { |
| return Promise.reject('RenderPrimitive does not have a material'); |
| } |
| |
| let completionPromises = []; |
| |
| for (let attributeBuffer of this._attributeBuffers) { |
| if (!attributeBuffer._buffer._buffer) { |
| completionPromises.push(attributeBuffer._buffer._promise); |
| } |
| } |
| |
| if (this._indexBuffer && !this._indexBuffer._buffer) { |
| completionPromises.push(this._indexBuffer._promise); |
| } |
| |
| this._promise = Promise.all(completionPromises).then(() => { |
| this._complete = true; |
| return this; |
| }); |
| } |
| return this._promise; |
| } |
| } |
| |
| export class RenderTexture { |
| constructor(texture) { |
| this._texture = texture; |
| this._complete = false; |
| this._activeFrameId = 0; |
| this._activeCallback = null; |
| } |
| |
| markActive(frameId) { |
| if (this._activeCallback && this._activeFrameId != frameId) { |
| this._activeFrameId = frameId; |
| this._activeCallback(this); |
| } |
| } |
| } |
| |
| const inverseMatrix = mat4.create(); |
| |
| function setCap(gl, glEnum, cap, prevState, state) { |
| let change = (state & cap) - (prevState & cap); |
| if (!change) { |
| return; |
| } |
| |
| if (change > 0) { |
| gl.enable(glEnum); |
| } else { |
| gl.disable(glEnum); |
| } |
| } |
| |
| class RenderMaterialSampler { |
| constructor(renderer, materialSampler, index) { |
| this._renderer = renderer; |
| this._uniformName = materialSampler._uniformName; |
| this._renderTexture = renderer._getRenderTexture(materialSampler._texture); |
| this._index = index; |
| } |
| |
| set texture(value) { |
| this._renderTexture = this._renderer._getRenderTexture(value); |
| } |
| } |
| |
| class RenderMaterialUniform { |
| constructor(materialUniform) { |
| this._uniformName = materialUniform._uniformName; |
| this._uniform = null; |
| this._length = materialUniform._length; |
| if (materialUniform._value instanceof Array) { |
| this._value = new Float32Array(materialUniform._value); |
| } else { |
| this._value = new Float32Array([materialUniform._value]); |
| } |
| } |
| |
| set value(value) { |
| if (this._value.length == 1) { |
| this._value[0] = value; |
| } else { |
| for (let i = 0; i < this._value.length; ++i) { |
| this._value[i] = value[i]; |
| } |
| } |
| } |
| } |
| |
| class RenderMaterial { |
| constructor(renderer, material, program) { |
| this._program = program; |
| this._state = material.state._state; |
| this._activeFrameId = 0; |
| this._completeForActiveFrame = false; |
| |
| this._samplerDictionary = {}; |
| this._samplers = []; |
| for (let i = 0; i < material._samplers.length; ++i) { |
| let renderSampler = new RenderMaterialSampler(renderer, material._samplers[i], i); |
| this._samplers.push(renderSampler); |
| this._samplerDictionary[renderSampler._uniformName] = renderSampler; |
| } |
| |
| this._uniform_dictionary = {}; |
| this._uniforms = []; |
| for (let uniform of material._uniforms) { |
| let renderUniform = new RenderMaterialUniform(uniform); |
| this._uniforms.push(renderUniform); |
| this._uniform_dictionary[renderUniform._uniformName] = renderUniform; |
| } |
| |
| this._firstBind = true; |
| |
| this._renderOrder = material.renderOrder; |
| if (this._renderOrder == RENDER_ORDER.DEFAULT) { |
| if (this._state & CAP.BLEND) { |
| this._renderOrder = RENDER_ORDER.TRANSPARENT; |
| } else { |
| this._renderOrder = RENDER_ORDER.OPAQUE; |
| } |
| } |
| } |
| |
| bind(gl) { |
| // First time we do a binding, cache the uniform locations and remove |
| // unused uniforms from the list. |
| if (this._firstBind) { |
| for (let i = 0; i < this._samplers.length;) { |
| let sampler = this._samplers[i]; |
| if (!this._program.uniform[sampler._uniformName]) { |
| this._samplers.splice(i, 1); |
| continue; |
| } |
| ++i; |
| } |
| |
| for (let i = 0; i < this._uniforms.length;) { |
| let uniform = this._uniforms[i]; |
| uniform._uniform = this._program.uniform[uniform._uniformName]; |
| if (!uniform._uniform) { |
| this._uniforms.splice(i, 1); |
| continue; |
| } |
| ++i; |
| } |
| this._firstBind = false; |
| } |
| |
| for (let sampler of this._samplers) { |
| gl.activeTexture(gl.TEXTURE0 + sampler._index); |
| if (sampler._renderTexture && sampler._renderTexture._complete) { |
| gl.bindTexture(gl.TEXTURE_2D, sampler._renderTexture._texture); |
| } else { |
| gl.bindTexture(gl.TEXTURE_2D, null); |
| } |
| } |
| |
| for (let uniform of this._uniforms) { |
| switch (uniform._length) { |
| case 1: gl.uniform1fv(uniform._uniform, uniform._value); break; |
| case 2: gl.uniform2fv(uniform._uniform, uniform._value); break; |
| case 3: gl.uniform3fv(uniform._uniform, uniform._value); break; |
| case 4: gl.uniform4fv(uniform._uniform, uniform._value); break; |
| } |
| } |
| } |
| |
| markActive(frameId) { |
| if (this._activeFrameId != frameId) { |
| this._activeFrameId = frameId; |
| this._completeForActiveFrame = true; |
| for (let i = 0; i < this._samplers.length; ++i) { |
| let sampler = this._samplers[i]; |
| if (sampler._renderTexture) { |
| if (!sampler._renderTexture._complete) { |
| this._completeForActiveFrame = false; |
| break; |
| } |
| sampler._renderTexture.markActive(frameId); |
| } |
| } |
| } |
| return this._completeForActiveFrame; |
| } |
| |
| // Material State fetchers |
| get cullFace() { |
| return !!(this._state & CAP.CULL_FACE); |
| } |
| get blend() { |
| return !!(this._state & CAP.BLEND); |
| } |
| get depthTest() { |
| return !!(this._state & CAP.DEPTH_TEST); |
| } |
| get stencilTest() { |
| return !!(this._state & CAP.STENCIL_TEST); |
| } |
| get colorMask() { |
| return !!(this._state & CAP.COLOR_MASK); |
| } |
| get depthMask() { |
| return !!(this._state & CAP.DEPTH_MASK); |
| } |
| get stencilMask() { |
| return !!(this._state & CAP.STENCIL_MASK); |
| } |
| get depthFunc() { |
| return ((this._state & MAT_STATE.DEPTH_FUNC_RANGE) >> MAT_STATE.DEPTH_FUNC_SHIFT) + GL.NEVER; |
| } |
| get blendFuncSrc() { |
| return stateToBlendFunc(this._state, MAT_STATE.BLEND_SRC_RANGE, MAT_STATE.BLEND_SRC_SHIFT); |
| } |
| get blendFuncDst() { |
| return stateToBlendFunc(this._state, MAT_STATE.BLEND_DST_RANGE, MAT_STATE.BLEND_DST_SHIFT); |
| } |
| |
| // Only really for use from the renderer |
| _capsDiff(otherState) { |
| return (otherState & MAT_STATE.CAPS_RANGE) ^ (this._state & MAT_STATE.CAPS_RANGE); |
| } |
| |
| _blendDiff(otherState) { |
| if (!(this._state & CAP.BLEND)) { |
| return 0; |
| } |
| return (otherState & MAT_STATE.BLEND_FUNC_RANGE) ^ (this._state & MAT_STATE.BLEND_FUNC_RANGE); |
| } |
| |
| _depthFuncDiff(otherState) { |
| if (!(this._state & CAP.DEPTH_TEST)) { |
| return 0; |
| } |
| return (otherState & MAT_STATE.DEPTH_FUNC_RANGE) ^ (this._state & MAT_STATE.DEPTH_FUNC_RANGE); |
| } |
| } |
| |
| export class Renderer { |
| constructor(gl) { |
| this._gl = gl || createWebGLContext(); |
| this._frameId = 0; |
| this._programCache = {}; |
| this._textureCache = {}; |
| this._renderPrimitives = Array(RENDER_ORDER.DEFAULT); |
| this._cameraPositions = []; |
| |
| this._vaoExt = gl.getExtension('OES_vertex_array_object'); |
| |
| let fragHighPrecision = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); |
| this._defaultFragPrecision = fragHighPrecision.precision > 0 ? 'highp' : 'mediump'; |
| |
| this._depthMaskNeedsReset = false; |
| this._colorMaskNeedsReset = false; |
| |
| this._globalLightColor = vec3.clone(DEF_LIGHT_COLOR); |
| this._globalLightDir = vec3.clone(DEF_LIGHT_DIR); |
| } |
| |
| get gl() { |
| return this._gl; |
| } |
| |
| set globalLightColor(value) { |
| vec3.copy(this._globalLightColor, value); |
| } |
| |
| get globalLightColor() { |
| return vec3.clone(this._globalLightColor); |
| } |
| |
| set globalLightDir(value) { |
| vec3.copy(this._globalLightDir, value); |
| } |
| |
| get globalLightDir() { |
| return vec3.clone(this._globalLightDir); |
| } |
| |
| createRenderBuffer(target, data, usage = GL.STATIC_DRAW) { |
| let gl = this._gl; |
| let glBuffer = gl.createBuffer(); |
| |
| if (data instanceof Promise) { |
| let renderBuffer = new RenderBuffer(target, usage, data.then((data) => { |
| gl.bindBuffer(target, glBuffer); |
| gl.bufferData(target, data, usage); |
| renderBuffer._length = data.byteLength; |
| return glBuffer; |
| })); |
| return renderBuffer; |
| } else { |
| gl.bindBuffer(target, glBuffer); |
| gl.bufferData(target, data, usage); |
| return new RenderBuffer(target, usage, glBuffer, data.byteLength); |
| } |
| } |
| |
| updateRenderBuffer(buffer, data, offset = 0) { |
| if (buffer._buffer) { |
| let gl = this._gl; |
| gl.bindBuffer(buffer._target, buffer._buffer); |
| if (offset == 0 && buffer._length == data.byteLength) { |
| gl.bufferData(buffer._target, data, buffer._usage); |
| } else { |
| gl.bufferSubData(buffer._target, offset, data); |
| } |
| } else { |
| buffer.waitForComplete().then((buffer) => { |
| this.updateRenderBuffer(buffer, data, offset); |
| }); |
| } |
| } |
| |
| createRenderPrimitive(primitive, material) { |
| let renderPrimitive = new RenderPrimitive(primitive); |
| |
| let program = this._getMaterialProgram(material, renderPrimitive); |
| let renderMaterial = new RenderMaterial(this, material, program); |
| renderPrimitive.setRenderMaterial(renderMaterial); |
| |
| if (!this._renderPrimitives[renderMaterial._renderOrder]) { |
| this._renderPrimitives[renderMaterial._renderOrder] = []; |
| } |
| |
| this._renderPrimitives[renderMaterial._renderOrder].push(renderPrimitive); |
| |
| return renderPrimitive; |
| } |
| |
| createMesh(primitive, material) { |
| let meshNode = new Node(); |
| meshNode.addRenderPrimitive(this.createRenderPrimitive(primitive, material)); |
| return meshNode; |
| } |
| |
| drawViews(views, rootNode) { |
| if (!rootNode) { |
| return; |
| } |
| |
| let gl = this._gl; |
| this._frameId++; |
| |
| rootNode.markActive(this._frameId); |
| |
| // If there's only one view then flip the algorithm a bit so that we're only |
| // setting the viewport once. |
| if (views.length == 1 && views[0].viewport) { |
| let vp = views[0].viewport; |
| this._gl.viewport(vp.x, vp.y, vp.width, vp.height); |
| } |
| |
| // Get the positions of the 'camera' for each view matrix. |
| for (let i = 0; i < views.length; ++i) { |
| mat4.invert(inverseMatrix, views[i].viewMatrix); |
| |
| if (this._cameraPositions.length <= i) { |
| this._cameraPositions.push(vec3.create()); |
| } |
| let cameraPosition = this._cameraPositions[i]; |
| vec3.set(cameraPosition, 0, 0, 0); |
| vec3.transformMat4(cameraPosition, cameraPosition, inverseMatrix); |
| } |
| |
| // Draw each set of render primitives in order |
| for (let renderPrimitives of this._renderPrimitives) { |
| if (renderPrimitives && renderPrimitives.length) { |
| this._drawRenderPrimitiveSet(views, renderPrimitives); |
| } |
| } |
| |
| if (this._vaoExt) { |
| this._vaoExt.bindVertexArrayOES(null); |
| } |
| |
| if (this._depthMaskNeedsReset) { |
| gl.depthMask(true); |
| } |
| if (this._colorMaskNeedsReset) { |
| gl.colorMask(true, true, true, true); |
| } |
| } |
| |
| _drawRenderPrimitiveSet(views, renderPrimitives) { |
| let gl = this._gl; |
| let program = null; |
| let material = null; |
| let attribMask = 0; |
| |
| // Loop through every primitive known to the renderer. |
| for (let primitive of renderPrimitives) { |
| // Skip over those that haven't been marked as active for this frame. |
| if (primitive._activeFrameId != this._frameId) { |
| continue; |
| } |
| |
| // Bind the primitive material's program if it's different than the one we |
| // were using for the previous primitive. |
| // TODO: The ording of this could be more efficient. |
| if (program != primitive._material._program) { |
| program = primitive._material._program; |
| program.use(); |
| |
| if (program.uniform.LIGHT_DIRECTION) { |
| gl.uniform3fv(program.uniform.LIGHT_DIRECTION, this._globalLightDir); |
| } |
| |
| if (program.uniform.LIGHT_COLOR) { |
| gl.uniform3fv(program.uniform.LIGHT_COLOR, this._globalLightColor); |
| } |
| |
| if (views.length == 1) { |
| gl.uniformMatrix4fv(program.uniform.PROJECTION_MATRIX, false, views[0].projectionMatrix); |
| gl.uniformMatrix4fv(program.uniform.VIEW_MATRIX, false, views[0].viewMatrix); |
| gl.uniform3fv(program.uniform.CAMERA_POSITION, this._cameraPositions[0]); |
| gl.uniform1i(program.uniform.EYE_INDEX, views[0].eyeIndex); |
| } |
| } |
| |
| if (material != primitive._material) { |
| this._bindMaterialState(primitive._material, material); |
| primitive._material.bind(gl, program, material); |
| material = primitive._material; |
| } |
| |
| if (this._vaoExt) { |
| if (primitive._vao) { |
| this._vaoExt.bindVertexArrayOES(primitive._vao); |
| } else { |
| primitive._vao = this._vaoExt.createVertexArrayOES(); |
| this._vaoExt.bindVertexArrayOES(primitive._vao); |
| this._bindPrimitive(primitive); |
| } |
| } else { |
| this._bindPrimitive(primitive, attribMask); |
| attribMask = primitive._attributeMask; |
| } |
| |
| for (let i = 0; i < views.length; ++i) { |
| let view = views[i]; |
| if (views.length > 1) { |
| if (view.viewport) { |
| let vp = view.viewport; |
| gl.viewport(vp.x, vp.y, vp.width, vp.height); |
| } |
| gl.uniformMatrix4fv(program.uniform.PROJECTION_MATRIX, false, view.projectionMatrix); |
| gl.uniformMatrix4fv(program.uniform.VIEW_MATRIX, false, view.viewMatrix); |
| gl.uniform3fv(program.uniform.CAMERA_POSITION, this._cameraPositions[i]); |
| gl.uniform1i(program.uniform.EYE_INDEX, view.eyeIndex); |
| } |
| |
| for (let instance of primitive._instances) { |
| if (instance._activeFrameId != this._frameId) { |
| continue; |
| } |
| |
| gl.uniformMatrix4fv(program.uniform.MODEL_MATRIX, false, instance.worldMatrix); |
| |
| if (primitive._indexBuffer) { |
| gl.drawElements(primitive._mode, primitive._elementCount, |
| primitive._indexType, primitive._indexByteOffset); |
| } else { |
| gl.drawArrays(primitive._mode, 0, primitive._elementCount); |
| } |
| } |
| } |
| } |
| } |
| |
| _getRenderTexture(texture) { |
| if (!texture) { |
| return null; |
| } |
| |
| let key = texture.textureKey; |
| if (!key) { |
| throw new Error('Texure does not have a valid key'); |
| } |
| |
| if (key in this._textureCache) { |
| return this._textureCache[key]; |
| } else { |
| let gl = this._gl; |
| let textureHandle = gl.createTexture(); |
| |
| let renderTexture = new RenderTexture(textureHandle); |
| this._textureCache[key] = renderTexture; |
| |
| if (texture instanceof DataTexture) { |
| gl.bindTexture(gl.TEXTURE_2D, textureHandle); |
| gl.texImage2D(gl.TEXTURE_2D, 0, texture.format, texture.width, texture.height, |
| 0, texture.format, texture._type, texture._data); |
| this._setSamplerParameters(texture); |
| renderTexture._complete = true; |
| } else { |
| texture.waitForComplete().then(() => { |
| gl.bindTexture(gl.TEXTURE_2D, textureHandle); |
| gl.texImage2D(gl.TEXTURE_2D, 0, texture.format, texture.format, gl.UNSIGNED_BYTE, texture.source); |
| this._setSamplerParameters(texture); |
| renderTexture._complete = true; |
| |
| if (texture instanceof VideoTexture) { |
| // Once the video starts playing, set a callback to update it's |
| // contents each frame. |
| texture._video.addEventListener('playing', () => { |
| renderTexture._activeCallback = () => { |
| if (!texture._video.paused && !texture._video.waiting) { |
| gl.bindTexture(gl.TEXTURE_2D, textureHandle); |
| gl.texImage2D(gl.TEXTURE_2D, 0, texture.format, texture.format, gl.UNSIGNED_BYTE, texture.source); |
| } |
| }; |
| }); |
| } |
| }); |
| } |
| |
| return renderTexture; |
| } |
| } |
| |
| _setSamplerParameters(texture) { |
| let gl = this._gl; |
| |
| let sampler = texture.sampler; |
| let powerOfTwo = isPowerOfTwo(texture.width) && isPowerOfTwo(texture.height); |
| let mipmap = powerOfTwo && texture.mipmap; |
| if (mipmap) { |
| gl.generateMipmap(gl.TEXTURE_2D); |
| } |
| |
| let minFilter = sampler.minFilter || (mipmap ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR); |
| let wrapS = sampler.wrapS || (powerOfTwo ? gl.REPEAT : gl.CLAMP_TO_EDGE); |
| let wrapT = sampler.wrapT || (powerOfTwo ? gl.REPEAT : gl.CLAMP_TO_EDGE); |
| |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, sampler.magFilter || gl.LINEAR); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT); |
| } |
| |
| _getProgramKey(name, defines) { |
| let key = `${name}:`; |
| |
| for (let define in defines) { |
| key += `${define}=${defines[define]},`; |
| } |
| |
| return key; |
| } |
| |
| _getMaterialProgram(material, renderPrimitive) { |
| let materialName = material.materialName; |
| let vertexSource = material.vertexSource; |
| let fragmentSource = material.fragmentSource; |
| |
| // These should always be defined for every material |
| if (materialName == null) { |
| throw new Error('Material does not have a name'); |
| } |
| if (vertexSource == null) { |
| throw new Error(`Material "${materialName}" does not have a vertex source`); |
| } |
| if (fragmentSource == null) { |
| throw new Error(`Material "${materialName}" does not have a fragment source`); |
| } |
| |
| let defines = material.getProgramDefines(renderPrimitive); |
| let key = this._getProgramKey(materialName, defines); |
| |
| if (key in this._programCache) { |
| return this._programCache[key]; |
| } else { |
| let multiview = false; // Handle this dynamically later |
| let fullVertexSource = vertexSource; |
| fullVertexSource += multiview ? VERTEX_SHADER_MULTI_ENTRY : |
| VERTEX_SHADER_SINGLE_ENTRY; |
| |
| let precisionMatch = fragmentSource.match(PRECISION_REGEX); |
| let fragPrecisionHeader = precisionMatch ? '' : `precision ${this._defaultFragPrecision} float;\n`; |
| |
| let fullFragmentSource = fragPrecisionHeader + fragmentSource; |
| fullFragmentSource += FRAGMENT_SHADER_ENTRY; |
| |
| let program = new Program(this._gl, fullVertexSource, fullFragmentSource, ATTRIB, defines); |
| this._programCache[key] = program; |
| |
| program.onNextUse((program) => { |
| // Bind the samplers to the right texture index. This is constant for |
| // the lifetime of the program. |
| for (let i = 0; i < material._samplers.length; ++i) { |
| let sampler = material._samplers[i]; |
| let uniform = program.uniform[sampler._uniformName]; |
| if (uniform) { |
| this._gl.uniform1i(uniform, i); |
| } |
| } |
| }); |
| |
| return program; |
| } |
| } |
| |
| _bindPrimitive(primitive, attribMask) { |
| let gl = this._gl; |
| |
| // If the active attributes have changed then update the active set. |
| if (attribMask != primitive._attributeMask) { |
| for (let attrib in ATTRIB) { |
| if (primitive._attributeMask & ATTRIB_MASK[attrib]) { |
| gl.enableVertexAttribArray(ATTRIB[attrib]); |
| } else { |
| gl.disableVertexAttribArray(ATTRIB[attrib]); |
| } |
| } |
| } |
| |
| // Bind the primitive attributes and indices. |
| for (let attributeBuffer of primitive._attributeBuffers) { |
| gl.bindBuffer(gl.ARRAY_BUFFER, attributeBuffer._buffer._buffer); |
| for (let attrib of attributeBuffer._attributes) { |
| gl.vertexAttribPointer( |
| attrib._attrib_index, attrib._componentCount, attrib._componentType, |
| attrib._normalized, attrib._stride, attrib._byteOffset); |
| } |
| } |
| |
| if (primitive._indexBuffer) { |
| gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, primitive._indexBuffer._buffer); |
| } else { |
| gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); |
| } |
| } |
| |
| _bindMaterialState(material, prevMaterial = null) { |
| let gl = this._gl; |
| |
| let state = material._state; |
| let prevState = prevMaterial ? prevMaterial._state : ~state; |
| |
| // Return early if both materials use identical state |
| if (state == prevState) { |
| return; |
| } |
| |
| // Any caps bits changed? |
| if (material._capsDiff(prevState)) { |
| setCap(gl, gl.CULL_FACE, CAP.CULL_FACE, prevState, state); |
| setCap(gl, gl.BLEND, CAP.BLEND, prevState, state); |
| setCap(gl, gl.DEPTH_TEST, CAP.DEPTH_TEST, prevState, state); |
| setCap(gl, gl.STENCIL_TEST, CAP.STENCIL_TEST, prevState, state); |
| |
| let colorMaskChange = (state & CAP.COLOR_MASK) - (prevState & CAP.COLOR_MASK); |
| if (colorMaskChange) { |
| let mask = colorMaskChange > 1; |
| this._colorMaskNeedsReset = !mask; |
| gl.colorMask(mask, mask, mask, mask); |
| } |
| |
| let depthMaskChange = (state & CAP.DEPTH_MASK) - (prevState & CAP.DEPTH_MASK); |
| if (depthMaskChange) { |
| this._depthMaskNeedsReset = !(depthMaskChange > 1); |
| gl.depthMask(depthMaskChange > 1); |
| } |
| |
| let stencilMaskChange = (state & CAP.STENCIL_MASK) - (prevState & CAP.STENCIL_MASK); |
| if (stencilMaskChange) { |
| gl.stencilMask(stencilMaskChange > 1); |
| } |
| } |
| |
| // Blending enabled and blend func changed? |
| if (material._blendDiff(prevState)) { |
| gl.blendFunc(material.blendFuncSrc, material.blendFuncDst); |
| } |
| |
| // Depth testing enabled and depth func changed? |
| if (material._depthFuncDiff(prevState)) { |
| gl.depthFunc(material.depthFunc); |
| } |
| } |
| } |