src/viewer/scene/webgl/WebGLRenderer.js
import {Map} from "../utils/Map.js";
const ids = new Map({});
import {math} from "../math/math.js";
const tempVec3a = math.vec3();
const tempVec4 = math.vec4();
import {LinearEncoding, sRGBEncoding} from "../constants/constants.js";
const TEXTURE_DECODE_FUNCS = { [sRGBEncoding]: "sRGBToLinear" };
const iota = function(n) {
const ret = [ ];
for (let i = 0; i < n; ++i) ret.push(i);
return ret;
};
export const createProgramVariablesState = function() {
const vertAppenders = [ ];
const vOutAppenders = [ ];
const fragAppenders = [ ];
const fragDefAppenders = [ ];
const attrSetters = [ ];
const attrHahes = [ ];
const unifSetters = [ ];
const createVertexDefinition = (name, appendDefinition) => {
let needed = false;
vertAppenders.push((src) => needed && appendDefinition(name, src));
return {
toString: () => {
needed = true;
return name;
}
};
};
const programVariables = {
createAttribute: function(type, name) {
let needed = false;
vertAppenders.push((src) => needed && src.push(`in ${type} ${name};`));
const ret = {
toString: () => {
needed = true;
return name;
}
};
attrSetters.push((getInputSetter) => {
const setValue = needed && getInputSetter(name);
setValue && attrHahes.push(setValue.attributeHash);
ret.setInputValue = setValue;
return null;
});
return ret;
},
createFragmentDefinition: (name, appendDefinition) => {
let needed = false;
fragDefAppenders.push((src) => needed && appendDefinition(name, src));
return {
toString: () => {
needed = true;
return name;
}
};
},
createOutput: (type, name, location) => {
let needed = false;
fragAppenders.push((src) => needed && src.push(`layout(location = ${location || 0}) out ${type} ${name};`));
return {
toString: () => {
needed = true;
return name;
}
};
},
createUniform: (type, name, valueSetter) => {
let needed = false;
const append = (src) => needed && src.push(`uniform ${type} ${name};`);
vertAppenders.push(append);
fragAppenders.push(append);
const ret = {
toString: () => {
needed = true;
return name;
}
};
unifSetters.push((getInputSetter) => {
const setValue = needed && getInputSetter(name);
if (valueSetter) {
return setValue && ((state) => valueSetter(setValue, state));
} else {
ret.setInputValue = setValue;
return null;
}
});
return ret;
},
createUniformArray: (type, name, length) => {
let needed = false;
const append = (src) => needed && src.push(`uniform ${type} ${name}[${length}];`);
vertAppenders.push(append);
fragAppenders.push(append);
const ret = {
toString: () => {
needed = true;
return name;
}
};
unifSetters.push((getInputSetter) => {
ret.setInputValue = needed && getInputSetter(name);
return null;
});
return ret;
},
createUniformBlock: (name, types, valueSetter) => {
let needed = false;
const keys = Object.keys(types);
const append = (src) => {
if (needed) {
src.push(`uniform ${name} {`);
keys.forEach(k => src.push(` ${types[k]} ${k};`));
src.push(`};`);
}
};
vertAppenders.push(append);
fragAppenders.push(append);
const ret = { };
keys.forEach(k => ret[k] = {
toString: () => {
needed = true;
return k;
}
});
unifSetters.push((getInputSetter) => {
const setValue = needed && getInputSetter(name);
if (valueSetter) {
return setValue && ((state) => valueSetter(setValue, state));
} else {
ret.setInputValue = setValue;
return null;
}
});
return ret;
},
createVarying: (type, name, genValueCode, interpolationQualifier) => {
let needed = false;
const intp = interpolationQualifier ? (interpolationQualifier + " ") : "";
vertAppenders.push((src) => needed && src.push(`${intp}out ${type} ${name};`));
vOutAppenders.push((src) => needed && src.push(`${name} = ${genValueCode()};`));
fragAppenders.push((src) => needed && src.push(`${intp}in ${type} ${name};`));
return {
toString: () => {
needed = true;
return name;
}
};
},
createVertexDefinition: createVertexDefinition,
commonLibrary: {
octDecode: createVertexDefinition(
"octDecode",
(name, src) => {
src.push(`vec3 ${name}(vec2 oct) {`);
src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
src.push(" if (v.z < 0.0) {");
src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
src.push(" }");
src.push(" return normalize(v);");
src.push("}");
})
}
};
return {
programVariables: programVariables,
buildProgram: (gl, programName, cfg) => {
const getLogDepth = cfg.getLogDepth;
const clipPos = cfg.clipPos;
const clipTransformSetup = cfg.usePickClipPos && (function() {
const pickClipPos = programVariables.createUniform("vec2", "pickClipPos");
const pickClipPosInv = programVariables.createUniform("vec2", "pickClipPosInv");
return {
setClipPosInputValue: frameCtx => {
pickClipPos.setInputValue(frameCtx.pickClipPos);
pickClipPosInv.setInputValue(frameCtx.pickClipPosInv);
},
transformedClipPos: () => `vec4((${clipPos}.xy / ${clipPos}.w - ${pickClipPos}) * ${pickClipPosInv} * ${clipPos}.w, ${clipPos}.zw)`
};
})();
const vertexClipPosition = clipTransformSetup ? clipTransformSetup.transformedClipPos() : clipPos;
const fragmentOutputs = [ ];
const isPerspective = programVariables.createVarying("float", "isPerspective", () => `(${cfg.projMatrix}[2][3] == -1.0) ? 1.0 : 0.0`);
const logDepthBufFC = programVariables.createUniform("float", "logDepthBufFC", (set, state) => set(2.0 / (Math.log(state.view.far + 1.0) / Math.LN2)));
const vFragDepth = programVariables.createVarying("float", "vFragDepth", () => "1.0 + clipPos.w");
if (getLogDepth) {
fragmentOutputs.push(`gl_FragDepth = ${cfg.testPerspectiveForGl_FragDepth ? `${isPerspective} == 0.0 ? gl_FragCoord.z : ` : ""}log2(${getLogDepth(vFragDepth)}) * ${logDepthBufFC} * 0.5;`);
} else if (cfg.cleanerEdges) {
fragmentOutputs.push(`gl_FragDepth = gl_FragCoord.z + length(vec2(dFdx(gl_FragCoord.z), dFdy(gl_FragCoord.z)));`);
}
const linearToGamma = programVariables.createFragmentDefinition(
"linearToGamma",
(name, src) => {
src.push(`vec4 ${name}(in vec4 value, in float gammaFactor) {`);
src.push(" return vec4(pow(value.xyz, vec3(1.0 / gammaFactor)), value.w);");
src.push("}");
});
const sectionPlanesState = cfg.sectionPlanesState;
const getClippingDistance = sectionPlanesState && (function() {
const allocatedUniforms = iota(sectionPlanesState.getNumAllocatedSectionPlanes()).map(i => {
const sectionPlaneUniform = (type, postfix, getValue) => {
return programVariables.createUniform(type, `sectionPlane${postfix}${i}`, (set, state) => {
const sectionPlanes = sectionPlanesState.sectionPlanes;
const numSectionPlanes = sectionPlanes.length;
const baseIndex = (state.mesh.layerIndex || 0) * numSectionPlanes;
const active = (i < numSectionPlanes) && state.mesh.renderFlags.sectionPlanesActivePerLayer[baseIndex + i];
return getValue(set, active, sectionPlanes[i], state.mesh.origin);
});
};
return {
act: sectionPlaneUniform("bool", "Active", (set, active) => set(active ? 1 : 0)),
dir: sectionPlaneUniform("vec3", "Dir", (set, active, plane) => active && set(plane.dir)),
pos: sectionPlaneUniform("vec3", "Pos", (set, active, plane, orig) => {
return active && set(orig
? math.mulVec3Scalar(math.normalizeVec3(plane.dir, tempVec3a), -(math.dotVec3(plane.dir, orig) + plane.dist), tempVec3a) // getPlaneRTCPos
: plane.pos);
})
};
});
return (allocatedUniforms.length > 0) && ((worldPosition) => allocatedUniforms.map(a => `(${a.act} ? clamp(dot(-${a.dir}, ${worldPosition} - ${a.pos}), 0.0, 1000.0) : 0.0)`).join(" + "));
})();
const crossSections = cfg.crossSections;
const sliceColorOr = (getClippingDistance
? (function() {
const sliceColor = programVariables.createUniform("vec4", "sliceColor", (set) => set(crossSections.sliceColor));
const sliceColorOr = color => {
sliceColorOr.needed = true;
return `(sliced ? ${sliceColor} : ${color})`;
};
return sliceColorOr;
})()
: (color => color));
const getGammaFactor = cfg.getGammaFactor;
const gammaFactor = programVariables.createUniform("float", "gammaFactor", (set) => set(getGammaFactor()));
cfg.appendFragmentOutputs(fragmentOutputs, getGammaFactor && ((color) => `${linearToGamma}(${color}, ${gammaFactor})`), "gl_FragCoord", sliceColorOr);
const fragmentClippingLines = (function() {
const src = [ ];
const sliceThickness = programVariables.createUniform("float", "sliceThickness", (set) => set(crossSections.sliceThickness));
if (getClippingDistance) {
if (sliceColorOr.needed) {
src.push(" bool sliced = false;");
}
src.push(` if (${cfg.clippableTest()}) {`);
const fragWorldPosition = programVariables.createVarying("highp vec3", "vHighpWorldPosition", () => `${cfg.worldPositionAttribute}.xyz`);
src.push(` float dist = ${getClippingDistance(fragWorldPosition)};`);
if (cfg.clippingCaps) {
const vClipPositionW = programVariables.createVarying("float", "vClipPositionW", () => "gl_Position.w");
src.push(` if (dist > (0.002 * ${vClipPositionW})) { discard; }`);
src.push(" if (dist > 0.0) { ");
src.push(` ${cfg.clippingCaps} = vec4(1.0, 0.0, 0.0, 1.0);`);
getLogDepth && src.push(` gl_FragDepth = log2(${getLogDepth(vFragDepth)}) * ${logDepthBufFC} * 0.5;`);
src.push(" return;");
src.push(" }");
} else {
src.push(` if (dist > ${sliceColorOr.needed ? sliceThickness : "0.0"}) { discard; }`);
}
if (sliceColorOr.needed) {
src.push(" sliced = dist > 0.0;");
}
src.push(" }");
}
return src;
})();
const fragDefinitions = (function() { const src = [ ]; fragDefAppenders.forEach(a => a(src)); return src; })();
const fragmentShader = [
...(function() { const src = [ ]; fragAppenders.forEach(a => a(src)); return src; })(),
...fragDefinitions,
"void main(void) {",
...(cfg.discardPoints
? [
" vec2 cxy = 2.0 * gl_PointCoord - 1.0;",
" if (dot(cxy, cxy) > 1.0) { discard; }"
]
: [ ]),
...fragmentClippingLines,
...fragmentOutputs,
"}"
];
const vertexOutputs = [
`gl_Position = ${vertexClipPosition};`,
...(cfg.getPointSize ? [ `gl_PointSize = ${cfg.getPointSize()};` ] : [ ]),
...(function() { const src = [ ]; vOutAppenders.forEach(a => a(src)); return src; })()
];
const vertexData = cfg.getVertexData ? cfg.getVertexData() : [ ];
const vertexShader = [
...(function() { const src = [ ]; vertAppenders.forEach(a => a(src)); return src; })(),
"void main(void) {",
...vertexData,
...vertexOutputs,
"}"
];
const preamble = (type) => [
"#version 300 es",
"// " + programName + " " + type + " shader",
"#ifdef GL_FRAGMENT_PRECISION_HIGH",
"precision highp float;",
"precision highp int;",
"precision highp usampler2D;",
"precision highp isampler2D;",
"precision highp sampler2D;",
"#else",
"precision mediump float;",
"precision mediump int;",
"precision mediump usampler2D;",
"precision mediump isampler2D;",
"precision mediump sampler2D;",
"#endif",
];
const vertSrc = preamble("vertex" ).concat(vertexShader);
const fragSrc = preamble("fragment").concat([
// Not the best place to define here, TODO: Move somewhere more appropriate after refactors
"vec4 sRGBToLinear(in vec4 value) {",
" return vec4(mix(pow(value.rgb * 0.9478672986 + 0.0521327014, vec3(2.4)), value.rgb * 0.0773993808, vec3(lessThanEqual(value.rgb, vec3(0.04045)))), value.w);",
"}"
]).concat(fragmentShader);
const makeShader = (type, srcLines) => {
const src = srcLines.join("\n");
const shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
return [ shader ];
} else {
return [ null,
[
"Failed to compile",
gl.getShaderInfoLog(shader),
src.split("\n").map((line, i) => (i + 1) + ": " + line + "\n").join("")
]
];
}
};
const [vertShader, vertErrors] = makeShader(gl.VERTEX_SHADER, vertSrc);
const [fragShader, fragErrors] = makeShader(gl.FRAGMENT_SHADER, fragSrc);
const errorObj = errors => {
console.error(errors.join("\n"));
return [ null, errors ];
};
if (vertErrors) {
return errorObj(["Vertex shader error"].concat(vertErrors));
} else if (fragErrors) {
return errorObj(["Fragment shader error"].concat(fragErrors));
} else {
const program = gl.createProgram();
if (! program) {
return errorObj(["Failed to allocate program"]);
} else {
gl.attachShader(program, vertShader);
gl.attachShader(program, fragShader);
gl.linkProgram(program);
// HACK: Disable validation temporarily
// Perhaps we should defer validation until render-time, when the program has values set for all inputs?
if (! gl.getProgramParameter(program, gl.LINK_STATUS)) {
return errorObj([
"",
gl.getProgramInfoLog(this.program),
"",
"Vertex shader:",
"",
...vertSrc,
"",
"Fragment shader:",
"",
...fragSrc
]);
} else {
const getInputSetter = makeInputSetters(gl, program);
attrSetters.forEach(i => i(getInputSetter));
const uSetters = unifSetters.map(i => i(getInputSetter)).filter(s => s);
const id = ids.addItem({});
return [ {
id: id,
bind: () => gl.useProgram(program),
inputSetters: {
attributesHash: attrHahes.sort().join(", "),
setUniforms: (frameCtx, state) => {
clipTransformSetup && clipTransformSetup.setClipPosInputValue(frameCtx);
uSetters.forEach(s => s(state));
}
},
destroy: () => {
ids.removeItem(id);
gl.deleteProgram(program);
gl.deleteShader(vertShader);
gl.deleteShader(fragShader);
}
} ];
}
}
}
}
};
};
export const createLightSetup = function(programVariables, lightsState) {
const lightAmbient = programVariables.createUniform("vec4", "lightAmbient", (set) => set(lightsState.getAmbientColorAndIntensity()));
const lights = lightsState.lights;
const directionals = lights.map((light, i) => {
const lightUniforms = {
color: programVariables.createUniform("vec4", `lightColor${i}`, (set) => {
const light = lights[i]; // in case it changed
tempVec4[0] = light.color[0];
tempVec4[1] = light.color[1];
tempVec4[2] = light.color[2];
tempVec4[3] = light.intensity;
set(tempVec4);
}),
position: programVariables.createUniform("vec3", `lightPos${i}`, (set) => set(lights[i].pos)),
direction: programVariables.createUniform("vec3", `lightDir${i}`, (set) => set(lights[i].dir)),
shadowProjMatrix: programVariables.createUniform("mat4", `shadowProjMatrix${i}`, (set) => set(lights[i].getShadowViewMatrix())),
shadowViewMatrix: programVariables.createUniform("mat4", `shadowViewMatrix${i}`, (set) => set(lights[i].getShadowViewMatrix())),
shadowMap: programVariables.createUniform("sampler2D", `shadowMap${i}`, (set) => set(lights[i].getShadowRenderBuf().colorTextures[0]))
};
const withViewLightDir = getDirection => {
return {
glslLight: {
isViewSpace: light.space === "view",
getColor: () => `${lightUniforms.color}.rgb * ${lightUniforms.color}.a`,
getDirection: (viewMatrix, viewPosition) => `normalize(${getDirection(viewMatrix, viewPosition)})`,
shadowParameters: light.castsShadow && {
getShadowProjMatrix: () => lightUniforms.shadowProjMatrix,
getShadowViewMatrix: () => lightUniforms.shadowViewMatrix,
getShadowMap: () => lightUniforms.shadowMap
}
},
setupLightsInputs: () => {
const setters = Object.values(lightUniforms).map(u => u.setupLightsInputs()).filter(v => v);
return () => setters.forEach(setState => setState());
}
};
};
if (light.type === "dir") {
if (light.space === "view") {
return withViewLightDir((viewMatrix, viewPosition) => `-${lightUniforms.direction}`);
} else {
// If normal mapping, the fragment->light vector will be in tangent space
return withViewLightDir((viewMatrix, viewPosition) => `-(${viewMatrix} * vec4(${lightUniforms.direction}, 0.0)).xyz`);
}
} else if (light.type === "point") {
if (light.space === "view") {
return withViewLightDir((viewMatrix, viewPosition) => `${lightUniforms.position} - ${viewPosition}`);
} else {
// If normal mapping, the fragment->light vector will be in tangent space
return withViewLightDir((viewMatrix, viewPosition) => `(${viewMatrix} * vec4(${lightUniforms.position}, 1.0)).xyz - ${viewPosition}`);
}
} else {
return null;
}
}).filter(v => v);
const setupCubeTexture = (name, getMaps) => {
const getValue = () => { const m = getMaps(); return (m.length > 0) && m[0]; };
const initMap = getValue();
return initMap && setupTexture(programVariables, "samplerCube", name, initMap.encoding, (set) => { const v = getValue(); v && set(v.texture); });
};
const lightMap = setupCubeTexture("light", () => lightsState.lightMaps);
const reflectionMap = setupCubeTexture("reflection", () => lightsState.reflectionMaps);
return {
getHash: () => lightsState.getHash(),
getAmbientColor: () => `${lightAmbient}.rgb * ${lightAmbient}.a`,
directionalLights: directionals.map(light => light.glslLight),
getIrradiance: lightMap && ((worldNormal) => `${lightMap(worldNormal)}.rgb`),
getReflection: reflectionMap && ((reflectVec, mipLevel) => `${reflectionMap(reflectVec, mipLevel)}.rgb`)
};
};
export const setupTexture = (programVariables, type, name, encoding, getTexture) => {
const map = programVariables.createUniform(type, name + "Map", getTexture);
return (texturePos, bias) => {
const texel = (bias
? `texture(${map}, ${texturePos}, ${bias})`
: `texture(${map}, ${texturePos})`);
return (encoding !== LinearEncoding) ? `${TEXTURE_DECODE_FUNCS[encoding]}(${texel})` : texel;
};
};
const makeInputSetters = function(gl, handle) {
const activeInputs = { };
const numAttributes = gl.getProgramParameter(handle, gl.ACTIVE_ATTRIBUTES);
for (let i = 0; i < numAttributes; ++i) {
const attribute = gl.getActiveAttrib(handle, i);
const location = gl.getAttribLocation(handle, attribute.name);
activeInputs[attribute.name] = function(arrayBuf) {
arrayBuf.bindAtLocation(location);
gl.enableVertexAttribArray(location);
const divisor = arrayBuf.attributeDivisor;
if (divisor) {
gl.vertexAttribDivisor(location, divisor);
}
};
activeInputs[attribute.name].attributeHash = `${location.toString().padStart(1+Math.floor(Math.log10(numAttributes)), "0")}:${attribute.name}`;
}
const numBlocks = gl.getProgramParameter(handle, gl.ACTIVE_UNIFORM_BLOCKS);
for (let i = 0; i < numBlocks; ++i) {
const blockName = gl.getActiveUniformBlockName(handle, i);
const uniformBlockIndex = gl.getUniformBlockIndex(handle, blockName);
const uniformBlockBinding = i;
gl.uniformBlockBinding(handle, uniformBlockIndex, uniformBlockBinding);
const buffer = gl.createBuffer();
activeInputs[blockName] = function(data) {
gl.bindBuffer(gl.UNIFORM_BUFFER, buffer);
gl.bufferData(gl.UNIFORM_BUFFER, data, gl.DYNAMIC_DRAW);
gl.bindBufferBase(gl.UNIFORM_BUFFER, uniformBlockBinding, buffer);
};
}
const numUniforms = gl.getProgramParameter(handle, gl.ACTIVE_UNIFORMS);
let nextUnit = 0;
for (let i = 0; i < numUniforms; ++i) {
const u = gl.getActiveUniform(handle, i);
let uName = u.name;
if (uName.endsWith("[0]")) {
uName = uName.substr(0, uName.length - 3);
}
if (uName[uName.length - 1] === "\u0000") {
uName = uName.substr(0, uName.length - 1);
}
const location = gl.getUniformLocation(handle, uName);
const type = (function() {
for (let k in gl) {
if (u.type === gl[k])
return k;
}
return null;
})();
if ((u.type === gl.SAMPLER_2D)
||
(u.type === gl.SAMPLER_CUBE)
||
(u.type === gl.SAMPLER_2D_SHADOW)
||
((gl instanceof window.WebGL2RenderingContext)
&&
((u.type === gl.UNSIGNED_INT_SAMPLER_2D)
||
(u.type === gl.INT_SAMPLER_2D)))) {
const unit = nextUnit++;
activeInputs[uName] = function(texture) {
const bound = texture.bind(unit);
if (bound) {
gl.uniform1i(location, unit);
}
return bound;
};
} else {
activeInputs[uName] = (function() {
if (u.size === 1) {
switch (u.type) {
case gl.BOOL: return value => gl.uniform1i(location, value);
case gl.INT: return value => gl.uniform1i(location, value);
case gl.FLOAT: return value => gl.uniform1f(location, value);
case gl.FLOAT_VEC2: return value => gl.uniform2fv(location, value);
case gl.FLOAT_VEC3: return value => gl.uniform3fv(location, value);
case gl.FLOAT_VEC4: return value => gl.uniform4fv(location, value);
case gl.FLOAT_MAT3: return value => gl.uniformMatrix3fv(location, false, value);
case gl.FLOAT_MAT4: return value => gl.uniformMatrix4fv(location, false, value);
}
} else if (u.size > 1) {
switch (u.type) {
case gl.FLOAT: return value => gl.uniform1fv(location, value);
case gl.FLOAT_VEC2: return value => gl.uniform2fv(location, value);
case gl.FLOAT_VEC3: return value => gl.uniform3fv(location, value);
case gl.FLOAT_VEC4: return value => gl.uniform4fv(location, value);
}
}
throw `Unhandled uniform ${uName}`;
})();
}
}
return function(name) {
const u = activeInputs[name];
if (! u) {
throw `Missing input "${name}"`;
}
return u;
};
};