diff --git a/examples/high-mesh-count.html b/examples/high-mesh-count.html index 1e4b0ce5..3a9224b9 100644 --- a/examples/high-mesh-count.html +++ b/examples/high-mesh-count.html @@ -42,11 +42,13 @@ precision highp float; varying vec3 vNormal; + + uniform vec3 uColor; void main() { vec3 normal = normalize(vNormal); float lighting = dot(normal, normalize(vec3(-0.3, 0.8, 0.6))); - gl_FragColor.rgb = vec3(0.2, 0.8, 1.0) + lighting * 0.1; + gl_FragColor.rgb = uColor + lighting * 0.1; gl_FragColor.a = 1.0; } `; @@ -71,14 +73,25 @@ // Create base geometry const cubeGeometry = new Box(gl); - // Using the shader from the base primitive example - const program = new Program(gl, { - vertex, - fragment, - }); - // mesh container let meshes = []; + + const colorPrograms = {}; + const getColorProgram = () => { + const color = [ + ((Math.random() * 10) | 0) / 10, + ((Math.random() * 10) | 0) / 10, + ((Math.random() * 10) | 0) / 10, + ]; + + const key = color.join(','); + + return colorPrograms[key] || (colorPrograms[key] = new Program(gl, { + vertex, + fragment, + uniforms: { uColor: { value: color } } + })); + }; window.setMeshCount = function setMeshCount(count) { @@ -91,7 +104,7 @@ // create our meshes according to input for (let i = 0; i < count; ++i){ - let mesh = new Mesh(gl, {geometry: cubeGeometry, program}); + let mesh = new Mesh(gl, {geometry: cubeGeometry, program: getColorProgram()}); // position meshes in a random position between -100 / +100 in each dimension mesh.position.set( diff --git a/src/core/Program.js b/src/core/Program.js index f33bb252..5b4eb360 100644 --- a/src/core/Program.js +++ b/src/core/Program.js @@ -2,6 +2,8 @@ // TODO: upload identity matrix if null ? // TODO: sampler Cube +import { ProgramData } from "./ProgramData.js"; + let ID = 1; // cache of typed arrays used to flatten uniform arrays @@ -47,68 +49,34 @@ export class Program { else this.setBlendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); } - // compile vertex shader and log errors - const vertexShader = gl.createShader(gl.VERTEX_SHADER); - gl.shaderSource(vertexShader, vertex); - gl.compileShader(vertexShader); - if (gl.getShaderInfoLog(vertexShader) !== '') { - console.warn(`${gl.getShaderInfoLog(vertexShader)}\nVertex Shader\n${addLineNumbers(vertex)}`); - } + this.programData = ProgramData.create(gl, { vertex, fragment }); + } - // compile fragment shader and log errors - const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); - gl.shaderSource(fragmentShader, fragment); - gl.compileShader(fragmentShader); - if (gl.getShaderInfoLog(fragmentShader) !== '') { - console.warn(`${gl.getShaderInfoLog(fragmentShader)}\nFragment Shader\n${addLineNumbers(fragment)}`); - } + /** + * Only for backward compatibility + * Internally we not should use this + */ + get uniformLocations() { + return this.programData.uniformLocations; + } - // compile program and log errors - this.program = gl.createProgram(); - gl.attachShader(this.program, vertexShader); - gl.attachShader(this.program, fragmentShader); - gl.linkProgram(this.program); - if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { - return console.warn(gl.getProgramInfoLog(this.program)); - } + get attributeLocations() { + // we need this because Geometry use it + return this.programData.attributeLocations; + } - // Remove shader once linked - gl.deleteShader(vertexShader); - gl.deleteShader(fragmentShader); - - // Get active uniform locations - this.uniformLocations = new Map(); - let numUniforms = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS); - for (let uIndex = 0; uIndex < numUniforms; uIndex++) { - let uniform = gl.getActiveUniform(this.program, uIndex); - this.uniformLocations.set(uniform, gl.getUniformLocation(this.program, uniform.name)); - - // split uniforms' names to separate array and struct declarations - const split = uniform.name.match(/(\w+)/g); - - uniform.uniformName = split[0]; - - if (split.length === 3) { - uniform.isStructArray = true; - uniform.structIndex = Number(split[1]); - uniform.structProperty = split[2]; - } else if (split.length === 2 && isNaN(Number(split[1]))) { - uniform.isStruct = true; - uniform.structProperty = split[1]; - } - } + get attributeOrder() { + // we need this because a Geometry use it + return this.programData.attributeOrder; + } - // Get active attribute locations - this.attributeLocations = new Map(); - const locations = []; - const numAttribs = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES); - for (let aIndex = 0; aIndex < numAttribs; aIndex++) { - const attribute = gl.getActiveAttrib(this.program, aIndex); - const location = gl.getAttribLocation(this.program, attribute.name); - locations[location] = attribute.name; - this.attributeLocations.set(attribute, location); - } - this.attributeOrder = locations.join(''); + /** + * WebGLProgram instance, can be shared + * Only for backward compatibility + * Internally we not should use this + */ + get program() { + return this.programData.program; } setBlendFunc(src, dst, srcAlpha, dstAlpha) { @@ -145,20 +113,28 @@ export class Program { use({ flipFaces = false } = {}) { let textureUnit = -1; - const programActive = this.gl.renderer.state.currentProgram === this.id; + + /** + * @type {WebGL2RenderingContext} + */ + const gl = this.gl; + const uniforms = this.uniforms; + const programData = this.programData; + const uniformLocations = this.programData.uniformLocations; + const programActive = gl.renderer.state.currentProgram === programData.id; // Avoid gl call if program already in use if (!programActive) { - this.gl.useProgram(this.program); - this.gl.renderer.state.currentProgram = this.id; + gl.useProgram(programData.program); + gl.renderer.state.currentProgram = programData.id; } // Set only the active uniforms found in the shader - this.uniformLocations.forEach((location, activeUniform) => { + uniformLocations.forEach((location, activeUniform) => { let name = activeUniform.uniformName; // get supplied uniform - let uniform = this.uniforms[name]; + let uniform = uniforms[name]; // For structs, get the specific property instead of the entire object if (activeUniform.isStruct) { @@ -183,7 +159,7 @@ export class Program { // Check if texture needs to be updated uniform.value.update(textureUnit); - return setUniform(this.gl, activeUniform.type, location, textureUnit); + return setUniform(gl, activeUniform.type, location, textureUnit); } // For texture arrays, set uniform as an array of texture units instead of just one @@ -195,18 +171,19 @@ export class Program { textureUnits.push(textureUnit); }); - return setUniform(this.gl, activeUniform.type, location, textureUnits); + return setUniform(gl, activeUniform.type, location, textureUnits); } - setUniform(this.gl, activeUniform.type, location, uniform.value); + setUniform(gl, activeUniform.type, location, uniform.value); }); this.applyState(); - if (flipFaces) this.gl.renderer.setFrontFace(this.frontFace === this.gl.CCW ? this.gl.CW : this.gl.CCW); + if (flipFaces) gl.renderer.setFrontFace(this.frontFace === gl.CCW ? gl.CW : gl.CCW); } remove() { - this.gl.deleteProgram(this.program); + this.programData && this.programData.remove(); + this.programData = null; } } @@ -263,14 +240,6 @@ function setUniform(gl, type, location, value) { } } -function addLineNumbers(string) { - let lines = string.split('\n'); - for (let i = 0; i < lines.length; i++) { - lines[i] = i + 1 + ': ' + lines[i]; - } - return lines.join('\n'); -} - function flatten(a) { const arrayLen = a.length; const valueLen = a[0].length; diff --git a/src/core/ProgramData.js b/src/core/ProgramData.js new file mode 100644 index 00000000..cd0ec999 --- /dev/null +++ b/src/core/ProgramData.js @@ -0,0 +1,241 @@ +/** + * Internal program data class, storing shader data for each Program instance + * Used for reusing a native program for different Ogl programs without re-use of base shader. + */ + +let ID = 0; + +export class ProgramData { + /** + * @type {WeakMap>} + */ + static CACHE = new Map(); + + /** + * Create or return already existed program data for current shaders source + * @param { WebGLRenderingContext | WebGL2RenderingContext } gl + * @param {{ vertex: string, fragment: string}} param1 + * @returns {ProgramData} + */ + static create (gl, { vertex, fragment }) { + const store = ProgramData.CACHE.get(gl); + + if (!store) return new ProgramData(gl, { vertex, fragment }); + + const program = store.get(vertex + fragment); + + if (!program) return new ProgramData(gl, { vertex, fragment }); + + program.usage ++; + + return program; + } + + /** + * Store program data to cache + * @param { WebGLRenderingContext | WebGL2RenderingContext } gl + * @param { ProgramData } programData + * @returns { ProgramData } + */ + static set (gl, programData) { + const store = this.CACHE.get(gl) || new Map(); + + ProgramData.CACHE.set(gl, store); + + if (store.has(programData.vertex + programData.fragment)) { + console.warn( + '[ProgramData cache] Already have valid program data for this source:', + programData.vertex, + programData.fragment + ); + } + + store.set(programData.key, programData); + + return programData; + } + + /** + * @param { WebGLRenderingContext | WebGL2RenderingContext } gl + * @param { ProgramData } programData + */ + static delete (gl, programData) { + if (!programData || !programData.key) return false; + + const store = ProgramData.CACHE.get(gl); + + if (!store) return false; + + return store.delete(programData.key); + } + + constructor( + gl, + { + vertex, + fragment, + } + ) { + /** + * @type {WebGLRenderingContext | WebGL2RenderingContext} + */ + this.gl = gl; + + /** + * @type {string} + */ + this.vertex = vertex; + + /** + * @type {string} + */ + this.fragment = fragment; + + /** + * @type {WebGLProgram} + */ + this.program = null; + + /** + * @type {Map} + */ + this.uniformLocations = new Map(); + + /** + * @type {Map} + */ + this.attributeLocations = new Map() + + /** + * @type {string} + */ + this.attributeOrder = ''; + + this.id = (1 << 8) + ID++; + + this.usage = 0; + + this.compile(); + } + + get key() { + return this.vertex + this.fragment; + } + + /** + * Compile or validate exist program + * @returns { boolean } + */ + compile () { + const gl = this.gl; + const vertex = this.vertex; + const fragment = this.fragment; + + // check that compiled program still alive + if (this.program && gl.isProgram(this.program)) { + return true; + } + + // delete exist program for this context + // it can be invalid + ProgramData.delete(gl, this); + + // compile vertex shader and log errors + const vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, vertex); + gl.compileShader(vertexShader); + if (gl.getShaderInfoLog(vertexShader) !== '') { + console.warn(`${gl.getShaderInfoLog(vertexShader)}\nVertex Shader\n${addLineNumbers(vertex)}`); + } + + // compile fragment shader and log errors + const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragment); + gl.compileShader(fragmentShader); + if (gl.getShaderInfoLog(fragmentShader) !== '') { + console.warn(`${gl.getShaderInfoLog(fragmentShader)}\nFragment Shader\n${addLineNumbers(fragment)}`); + } + + // compile program and log errors + const program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.warn(gl.getProgramInfoLog(program)); + return false; + } + + // Remove shader once linked + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + + this.program = program; + // can be recompiled after loose + this.uniformLocations.clear(); + this.attributeLocations.clear(); + + // Get active uniform locations + let numUniforms = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS); + for (let uIndex = 0; uIndex < numUniforms; uIndex++) { + let uniform = gl.getActiveUniform(this.program, uIndex); + this.uniformLocations.set(uniform, gl.getUniformLocation(this.program, uniform.name)); + + // split uniforms' names to separate array and struct declarations + const split = uniform.name.match(/(\w+)/g); + + uniform.uniformName = split[0]; + + if (split.length === 3) { + uniform.isStructArray = true; + uniform.structIndex = Number(split[1]); + uniform.structProperty = split[2]; + } else if (split.length === 2 && isNaN(Number(split[1]))) { + uniform.isStruct = true; + uniform.structProperty = split[1]; + } + } + + // Get active attribute locations + const locations = []; + const numAttribs = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES); + for (let aIndex = 0; aIndex < numAttribs; aIndex++) { + const attribute = gl.getActiveAttrib(this.program, aIndex); + const location = gl.getAttribLocation(this.program, attribute.name); + locations[location] = attribute.name; + this.attributeLocations.set(attribute, location); + } + + this.attributeOrder = locations.join(''); + + // storing only valid programs + ProgramData.set(gl, this); + + return true; + } + + remove() { + this.usage--; + + if (this.usage <= 0 && this.program) { + this.gl.deleteProgram(this.program); + + ProgramData.delete(this.gl, this); + } + + this.id = -1; + this.fragment = null; + this.vertex = null; + this.attributeLocations.clear(); + this.attributeOrder = ''; + this.uniformLocations.clear(); + } +} + +function addLineNumbers(string) { + let lines = string.split('\n'); + for (let i = 0; i < lines.length; i++) { + lines[i] = i + 1 + ': ' + lines[i]; + } + return lines.join('\n'); +}