90 lines
12 KiB
JavaScript
90 lines
12 KiB
JavaScript
import {vs,fs,depthvs,depthfs} from './legacy-shaders.js';
|
|
/** Self-contained fallback. No external dependencies. Keeps the original shaders. */
|
|
export async function createLegacyRenderer({canvas,gltf,binary}) {
|
|
let frame=null,solar,interior,eye,vp,lightVP,gl,program,shadowProgram,shadowFbo,shadowTex;
|
|
const meshes=[],originalColors={},hardieTextures=[],hardieImages=[],hardieObjectURLs=[];
|
|
const mul=(a,b)=>{const c=new Float32Array(16);for(let j=0;j<4;j++)for(let i=0;i<4;i++)for(let k=0;k<4;k++)c[j*4+i]+=a[k*4+i]*b[j*4+k];return c;};
|
|
const lookAt=()=>new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]);
|
|
const ortho=lookAt;
|
|
function compile(type,src){const sh=gl.createShader(type);gl.shaderSource(sh,src);gl.compileShader(sh);if(!gl.getShaderParameter(sh,gl.COMPILE_STATUS))throw Error(gl.getShaderInfoLog(sh));return sh;}
|
|
function link(v,f){const pr=gl.createProgram();gl.attachShader(pr,compile(gl.VERTEX_SHADER,v));gl.attachShader(pr,compile(gl.FRAGMENT_SHADER,f));gl.linkProgram(pr);if(!gl.getProgramParameter(pr,gl.LINK_STATUS))throw Error(gl.getProgramInfoLog(pr));return pr;}
|
|
async function loadHardieTextures() {
|
|
const anisotropy = gl.getExtension('EXT_texture_filter_anisotropic');
|
|
for (const [index, texture] of (gltf.textures || []).entries()) {
|
|
const imageSpec = gltf.images[texture.source], view = gltf.bufferViews[imageSpec.bufferView];
|
|
const data = binary.subarray(view.byteOffset || 0, (view.byteOffset || 0) + view.byteLength);
|
|
const url = URL.createObjectURL(new Blob([data], { type: imageSpec.mimeType })); hardieObjectURLs.push(url);
|
|
const image = new Image();
|
|
await new Promise((resolve, reject) => { image.onload = resolve; image.onerror = () => reject(Error('Nelze na\u010d\u00edst vlo\u017eenou texturu HardiePlank.')); image.src = url; });
|
|
const gpuTexture = gl.createTexture(); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, gpuTexture);
|
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
gl.generateMipmap(gl.TEXTURE_2D);
|
|
if (anisotropy) gl.texParameterf(gl.TEXTURE_2D, anisotropy.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(8, gl.getParameter(anisotropy.MAX_TEXTURE_MAX_ANISOTROPY_EXT)));
|
|
hardieTextures[index] = gpuTexture; hardieImages[index] = image;
|
|
}
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
}
|
|
function getAccessor(i) { const a = gltf.accessors[i], v = gltf.bufferViews[a.bufferView]; return { a, v, offset: (v.byteOffset || 0) + (a.byteOffset || 0) }; }
|
|
function visible(m) { return frame.visible(m); }
|
|
function initGL() {
|
|
gl = canvas.getContext('webgl2', { antialias: true, alpha: false, preserveDrawingBuffer: true, powerPreference: 'high-performance' });
|
|
if (!gl) throw Error('Prohl\u00ed\u017ee\u010d nepodporuje WebGL 2. Soubor GLB si m\u016f\u017ee\u0161 st\u00e1hnout a otev\u0159\u00edt v 3D programu.');
|
|
program = link(vs, fs); shadowProgram = link(depthvs, depthfs);
|
|
const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, binary, gl.STATIC_DRAW);
|
|
const ibuf = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, binary, gl.STATIC_DRAW);
|
|
for (const [nodeIndex, node] of gltf.nodes.entries()) {
|
|
if (node.mesh === undefined) continue; const mesh = gltf.meshes[node.mesh]; for (const p of mesh.primitives) {
|
|
const pos = getAccessor(p.attributes.POSITION), nor = getAccessor(p.attributes.NORMAL), ind = getAccessor(p.indices), mat = gltf.materials[p.material], name = node.name || mesh.name || '';
|
|
if (node.matrix && node.matrix.some((v, i) => Math.abs(v - (i % 5 === 0 ? 1 : 0)) > 1e-6)) throw Error('Unexpected non-identity model transform');
|
|
const vao = gl.createVertexArray(); gl.bindVertexArray(vao); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, pos.a.componentType, false, pos.v.byteStride || 0, pos.offset); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 3, nor.a.componentType, false, nor.v.byteStride || 0, nor.offset); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf);
|
|
for (const [attribute, location, size] of [['TEXCOORD_0', 2, 2], ['TANGENT', 3, 4]]) {
|
|
if (p.attributes[attribute] !== undefined) { const a = getAccessor(p.attributes[attribute]); gl.enableVertexAttribArray(location); gl.vertexAttribPointer(location, size, a.a.componentType, false, a.v.byteStride || 0, a.offset); }
|
|
else { gl.disableVertexAttribArray(location); if (location === 2) gl.vertexAttrib2f(location, 0, 0); else gl.vertexAttrib4f(location, 1, 0, 0, 1); }
|
|
}
|
|
const pbr = mat.pbrMetallicRoughness || {}; originalColors[p.material] = [...(pbr.baseColorFactor || [.7, .7, .7, 1])];
|
|
meshes.push({ std: node.extras || {}, transparent: mat.alphaMode === 'BLEND', vao, count: ind.a.count, type: ind.a.componentType, offset: ind.offset, material: p.material, name, context: /^(90_|91_|28_|23_)/.test(name), nodeIndex, legacySite: !!node.extras?.legacySite, siteLayer: node.extras?.siteLayer || null, castShadow: node.extras?.castShadow !== false, bounds: { min: pos.a.min, max: pos.a.max }, glass: (mat.name || '').startsWith('glass'), interiorLayer: node.extras?.interiorLayer || null, interiorMaterial: !!mat.extras?.interiorMaterial, interiorStyle: mat.extras?.interiorStyle || 0, surfaceKind: mat.extras?.hardieRole === 'boards' ? (node.extras?.interiorLayer === 'cutShell' ? 3 : 1) : mat.extras?.hardieRole === 'backing' ? 2 : (mat.extras?.interiorMaterial || mat.extras?.linearColor) ? (pbr.baseColorTexture ? 3 : 2) : 0, pbr });
|
|
}
|
|
}
|
|
gl.bindVertexArray(null); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.disable(gl.CULL_FACE);
|
|
shadowTex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, shadowTex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, 2048, 2048, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
shadowFbo = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, shadowTex, 0); gl.drawBuffers([gl.NONE]); gl.readBuffer(gl.NONE); if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) throw Error('Shadow framebuffer incomplete'); gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
const lightPos = [-15, 29, 12], lightTarget = [.5, 0, 0]; lightVP = mul(ortho(-20, 20, -19, 19, 1, 65), lookAt(lightPos, lightTarget));
|
|
|
|
}
|
|
function renderShadow() { gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFbo); gl.viewport(0, 0, 2048, 2048); gl.clear(gl.DEPTH_BUFFER_BIT); gl.useProgram(shadowProgram); gl.uniformMatrix4fv(gl.getUniformLocation(shadowProgram, 'uVP'), false, lightVP); gl.enable(gl.POLYGON_OFFSET_FILL); gl.polygonOffset(2.0, 3.0); for (const m of meshes) { if (!visible(m) || m.glass || m.transparent || !m.castShadow) continue; gl.bindVertexArray(m.vao); gl.drawElements(gl.TRIANGLES, m.count, m.type, m.offset); } gl.disable(gl.POLYGON_OFFSET_FILL); gl.bindFramebuffer(gl.FRAMEBUFFER, null); }
|
|
function render() {
|
|
if (solar.shadowDirty && solar.shadows && solar.direct > 0) { renderShadow(); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, canvas.width, canvas.height); gl.clearColor(...solar.sky, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.useProgram(program); gl.uniformMatrix4fv(gl.getUniformLocation(program, 'uVP'), false, vp); gl.uniformMatrix4fv(gl.getUniformLocation(program, 'uLightVP'), false, lightVP); gl.uniform3fv(gl.getUniformLocation(program, 'uEye'), eye); gl.uniform3fv(gl.getUniformLocation(program, 'uLight'), solar.light); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, shadowTex); gl.uniform1i(gl.getUniformLocation(program, 'uShadow'), 0); gl.uniform1i(gl.getUniformLocation(program, 'uBaseMap'), 1); gl.uniform1i(gl.getUniformLocation(program, 'uNormalMap'), 2);
|
|
gl.uniform3fv(gl.getUniformLocation(program, 'uAmbient'), solar.ambient); gl.uniform3fv(gl.getUniformLocation(program, 'uSunColor'), solar.sunColor); gl.uniform1f(gl.getUniformLocation(program, 'uDirect'), solar.direct); gl.uniform1f(gl.getUniformLocation(program, 'uSkyDay'), solar.dayFactor); gl.uniform1f(gl.getUniformLocation(program, 'uShadows'), solar.shadows ? 1 : 0);
|
|
const transparentMeshes=meshes.filter(m=>m.transparent&&visible(m)).sort((a,b)=>{const dist=m=>{const c=m.bounds.min.map((v,i)=>(v+m.bounds.max[i])/2);return c.reduce((d,v,i)=>d+(v-eye[i])**2,0)};return dist(b)-dist(a);});
|
|
for (const m of meshes.filter(m=>!m.transparent).concat(transparentMeshes)) { if (!visible(m)) continue; const p = gltf.materials[m.material].pbrMetallicRoughness;
|
|
if(m.transparent){gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);gl.depthMask(false);}else{gl.disable(gl.BLEND);gl.depthMask(true);}
|
|
gl.uniform2fv(gl.getUniformLocation(program,'uUvScale'),p.baseColorTexture?.extensions?.KHR_texture_transform?.scale||[1,1]); gl.uniform4fv(gl.getUniformLocation(program, 'uColor'), p.baseColorFactor || [.7, .7, .7, 1]); gl.uniform1f(gl.getUniformLocation(program, 'uRoughness'), p.roughnessFactor || .8); gl.uniform1f(gl.getUniformLocation(program, 'uMetal'), p.metallicFactor || 0); gl.uniform1f(gl.getUniformLocation(program, 'uGlass'), m.glass ? 1 : 0);
|
|
gl.uniform1f(gl.getUniformLocation(program, 'uSurfaceKind'), m.surfaceKind);
|
|
gl.uniform1f(gl.getUniformLocation(program, 'uInteriorMaterial'), m.interiorMaterial ? 1 : 0);
|
|
gl.uniform1f(gl.getUniformLocation(program, 'uInteriorFill'), interior.enabled && interior.fill ? .30 : 0);
|
|
gl.uniform1f(gl.getUniformLocation(program, 'uInteriorStyle'), m.interiorStyle);
|
|
if (m.surfaceKind === 3) {gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,hardieTextures[p.baseColorTexture.index]);}
|
|
if (m.surfaceKind === 1) {
|
|
const mat = gltf.materials[m.material];
|
|
gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, hardieTextures[p.baseColorTexture.index]); gl.uniform1i(gl.getUniformLocation(program, 'uBaseMap'), 1);
|
|
gl.activeTexture(gl.TEXTURE2); gl.bindTexture(gl.TEXTURE_2D, hardieTextures[mat.normalTexture.index]); gl.uniform1i(gl.getUniformLocation(program, 'uNormalMap'), 2);
|
|
gl.uniform1f(gl.getUniformLocation(program, 'uNormalScale'), mat.normalTexture.scale ?? 1);
|
|
}
|
|
gl.bindVertexArray(m.vao); gl.drawElements(gl.TRIANGLES, m.count, m.type, m.offset); }
|
|
gl.depthMask(true);gl.disable(gl.BLEND);gl.bindVertexArray(null);
|
|
}
|
|
for(const node of gltf.nodes){
|
|
const different=(values,base)=>values&&values.some((v,i)=>Math.abs(v-base[i])>1e-6);
|
|
if(different(node.translation,[0,0,0])||different(node.rotation,[0,0,0,1])||different(node.scale,[1,1,1])||different(node.matrix,[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]))throw Error('Model has edited transforms; enable the Three.js renderer.');
|
|
}
|
|
initGL(); await loadHardieTextures();
|
|
return {backend:'local-webgl',label:'Lokalni WebGL / zaloha',meshes,images:hardieImages,
|
|
resize(w,h,dpr){canvas.width=Math.round(w*dpr);canvas.height=Math.round(h*dpr);},
|
|
draw(f){frame=f;({solar,interior,eye,vp,lightVP}=f);render();},
|
|
dispose(){hardieObjectURLs.forEach(URL.revokeObjectURL);}
|
|
};
|
|
}
|