UNPKG

@xeokit/xeokit-sdk

Version:

3D BIM IFC Viewer SDK for AEC engineering applications. Open Source JavaScript Toolkit based on pure WebGL for top performance, real-world coordinates and full double precision

613 lines (573 loc) 29.3 kB
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; }; };