diff --git a/src/engine/gfx/Renderer.ts b/src/engine/gfx/Renderer.ts index ac1e845..7216a70 100644 --- a/src/engine/gfx/Renderer.ts +++ b/src/engine/gfx/Renderer.ts @@ -27,7 +27,11 @@ export class Renderer { if (!this.renderer) { throw new Error('Renderer not yet initialized'); } - return !!this.renderer.extensions.get('ANGLE_instanced_arrays'); + const context = this.renderer.getContext(); + // Instancing is core in WebGL2; the WebGL1 extension may be absent. + return 'drawElementsInstanced' in context && 'vertexAttribDivisor' in context && + typeof context.drawElementsInstanced === 'function' && + typeof context.vertexAttribDivisor === 'function'; } initStats(container: HTMLElement): void { if (!this.stats) { diff --git a/src/engine/gfx/batch/InstancedMesh.ts b/src/engine/gfx/batch/InstancedMesh.ts index 118d46c..4d566bd 100644 --- a/src/engine/gfx/batch/InstancedMesh.ts +++ b/src/engine/gfx/batch/InstancedMesh.ts @@ -1,23 +1,12 @@ import * as THREE from 'three'; -const depthMaterial = new THREE.MeshDepthMaterial(); -depthMaterial.depthPacking = THREE.RGBADepthPacking; -(depthMaterial as any).clipping = true; -(depthMaterial as any).defines = { INSTANCE_TRANSFORM: "" }; -const distanceShader = THREE.ShaderLib.distance; -const distanceUniforms = THREE.UniformsUtils.clone(distanceShader.uniforms); -const distanceDefines = { USE_SHADOWMAP: "", INSTANCE_TRANSFORM: "" }; -const distanceMaterial = new THREE.ShaderMaterial({ - defines: distanceDefines, - uniforms: distanceUniforms, - vertexShader: distanceShader.vertexShader, - fragmentShader: distanceShader.fragmentShader, - clipping: true, -}); +import { installInstancingShader } from './instancingShader'; export class InstancedMesh extends THREE.Mesh { public maxInstances: number; public uniformScale: boolean; public useInstanceColor: boolean; private instanceMatrixAttributes: THREE.InstancedBufferAttribute[]; + private inverseWorldMatrix = new THREE.Matrix4(); + private localInstanceMatrix = new THREE.Matrix4(); constructor(geometry: THREE.BufferGeometry, material: THREE.Material, maxInstances: number, uniformScale: boolean, useInstanceColor: boolean = false) { const instancedGeometry = new THREE.InstancedBufferGeometry(); (instancedGeometry as any).copy(geometry); @@ -26,10 +15,10 @@ export class InstancedMesh extends THREE.Mesh { this.uniformScale = uniformScale; this.useInstanceColor = useInstanceColor; this.initAttributes(this.geometry as THREE.InstancedBufferGeometry); - this.material = this.decorateMaterial(material.clone()); + this.material = this.decorateMaterial(material.clone(), !!(material as any).palette); this.frustumCulled = false; - this.customDepthMaterial = depthMaterial; - this.customDistanceMaterial = distanceMaterial; + this.customDepthMaterial = this.decorateMaterial(new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking })); + this.customDistanceMaterial = this.decorateMaterial(new THREE.MeshDistanceMaterial()); } private initAttributes(geometry: THREE.InstancedBufferGeometry): void { const attributes: Array<{ @@ -69,7 +58,7 @@ export class InstancedMesh extends THREE.Mesh { .fill(0) .map((_, i) => geometry.getAttribute("instanceMatrix" + i) as THREE.InstancedBufferAttribute); } - private decorateMaterial(material: THREE.Material): THREE.Material { + private decorateMaterial(material: THREE.Material, palette = false): THREE.Material { const mat = material as any; if (!mat.defines) { mat.defines = {}; @@ -88,6 +77,7 @@ export class InstancedMesh extends THREE.Mesh { delete mat.defines.INSTANCE_COLOR; } mat.defines.INSTANCE_OPACITY = ""; + installInstancingShader(material, palette); return material; } public setRenderCount(count: number): void { @@ -110,9 +100,13 @@ export class InstancedMesh extends THREE.Mesh { const opacityAttr = attributes.instanceOpacity as THREE.InstancedBufferAttribute; const paletteOffsetAttr = attributes.instancePaletteOffset as THREE.InstancedBufferAttribute; const extraLightAttr = attributes.instanceExtraLight as THREE.InstancedBufferAttribute; + // Source matrices are in world space; the batch can have a transformed parent. + this.updateWorldMatrix(true, false); + this.inverseWorldMatrix.copy(this.matrixWorld).invert(); for (let i = 0, len = meshes.length; i < len; i++) { const mesh = meshes[i]; - this.setMatrixAt(i, mesh.matrixWorld); + this.localInstanceMatrix.multiplyMatrices(this.inverseWorldMatrix, mesh.matrixWorld); + this.setMatrixAt(i, this.localInstanceMatrix); const opacity = mesh.getOpacity(); if (opacityAttr.getX(i) !== opacity) { opacityAttr.setX(i, opacity); @@ -142,5 +136,7 @@ export class InstancedMesh extends THREE.Mesh { public dispose(): void { this.geometry.dispose(); (this.material as THREE.Material).dispose(); + this.customDepthMaterial?.dispose(); + this.customDistanceMaterial?.dispose(); } } diff --git a/src/engine/gfx/batch/MeshInstancingBatch.ts b/src/engine/gfx/batch/MeshInstancingBatch.ts index 0e471d5..4fb0ba6 100644 --- a/src/engine/gfx/batch/MeshInstancingBatch.ts +++ b/src/engine/gfx/batch/MeshInstancingBatch.ts @@ -35,7 +35,12 @@ export class MeshInstancingBatch { set clippingPlanes(value: THREE.Plane[]) { this._clippingPlanes = value; if (this.instancedMesh) { - (this.instancedMesh.material as any).clippingPlanes = value; + this.applyClippingPlanes(this.instancedMesh); + } + } + private applyClippingPlanes(mesh: InstancedMesh): void { + for (const material of [mesh.material as THREE.Material, mesh.customDepthMaterial, mesh.customDistanceMaterial]) { + if (material) material.clippingPlanes = this._clippingPlanes; } } get renderOrder(): number { @@ -69,8 +74,9 @@ export class MeshInstancingBatch { if (!this.instancedMesh) { this.instancedMesh = new InstancedMesh(meshes[0].geometry, meshes[0].material, this.maxInstances, true); this.instancedMesh.castShadow = this._castShadow; + this.instancedMesh.receiveShadow = this._receiveShadow; this.instancedMesh.renderOrder = this._renderOrder; - (this.instancedMesh.material as any).clippingPlanes = this._clippingPlanes; + this.applyClippingPlanes(this.instancedMesh); if (hasPalette) { const geometry = this.instancedMesh.geometry as THREE.InstancedBufferGeometry; geometry.setAttribute('instancePaletteOffset', new THREE.InstancedBufferAttribute(new Float32Array(this.maxInstances), 1)); diff --git a/src/engine/gfx/batch/instancingShader.ts b/src/engine/gfx/batch/instancingShader.ts new file mode 100644 index 0000000..b18b397 --- /dev/null +++ b/src/engine/gfx/batch/instancingShader.ts @@ -0,0 +1,44 @@ +import * as THREE from 'three'; + +/** Use the same instance transform in the color and both shadow passes. */ +export function installInstancingShader(material: THREE.Material, palette = false): void { + const beforeCompile = material.onBeforeCompile; + const cacheKey = material.customProgramCacheKey(); + material.onBeforeCompile = (shader, renderer) => { + beforeCompile.call(material, shader, renderer); + shader.vertexShader = shader.vertexShader + .replace('#include ', `#include +attribute vec4 instanceMatrix0; +attribute vec4 instanceMatrix1; +attribute vec4 instanceMatrix2; +attribute vec4 instanceMatrix3; +attribute float instanceOpacity; +varying float vInstanceOpacity; +mat4 getInstanceTransform() { + return mat4(instanceMatrix0, instanceMatrix1, instanceMatrix2, instanceMatrix3); +}`) + .replace('void main() {', `void main() { + vInstanceOpacity = instanceOpacity;`) + .replace('#include ', `#include + // As with Three.js instancing, rotation and nonuniform scale are supported; + // shear transforms are not used by voxel builders. + mat3 instanceNormal = mat3(getInstanceTransform()); + objectNormal /= vec3(dot(instanceNormal[0], instanceNormal[0]), + dot(instanceNormal[1], instanceNormal[1]), + dot(instanceNormal[2], instanceNormal[2])); + objectNormal = instanceNormal * objectNormal; + #ifdef USE_TANGENT + objectTangent = instanceNormal * objectTangent; + #endif`) + .replace('#include ', `#include + transformed = (getInstanceTransform() * vec4(transformed, 1.0)).xyz;`); + shader.fragmentShader = shader.fragmentShader.replace('#include ', + '#include \nvarying float vInstanceOpacity;'); + // Palette shaders already apply instanceOpacity when resolving the color. + if (!palette) { + shader.fragmentShader = shader.fragmentShader.replace('#include ', + 'diffuseColor.a *= vInstanceOpacity;\n#include '); + } + }; + material.customProgramCacheKey = () => `${cacheKey}|instance-transform-opacity-v1|${Number(palette)}`; +}