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');
+}