web-ifc-viewer
Version:
230 lines • 10.2 kB
JavaScript
import { Group, Mesh, MeshBasicMaterial, MeshDepthMaterial, OrthographicCamera, PlaneGeometry, ShaderMaterial, Vector3, WebGLRenderTarget } from 'three';
import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader';
import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader';
import { disposeMeshRecursively } from '../../utils/ThreeUtils';
export class ShadowDropper {
constructor(context, IFC) {
this.shadows = {};
// Controls how far away the shadow is computed
this.cameraHeight = 10;
this.darkness = 1.2;
this.opacity = 1;
this.resolution = 512;
this.amount = 3.5;
this.planeColor = 0xffffff;
this.shadowOffset = 0;
this.tempMaterial = new MeshBasicMaterial({ visible: false });
this.depthMaterial = new MeshDepthMaterial();
this.context = context;
this.IFC = IFC;
this.initializeDepthMaterial();
}
dispose() {
const shadowIDs = Object.keys(this.shadows);
shadowIDs.forEach((shadowID) => this.deleteShadow(shadowID));
this.shadows = null;
this.tempMaterial.dispose();
this.tempMaterial = null;
this.depthMaterial.dispose();
this.depthMaterial = null;
this.context = null;
this.IFC = null;
}
async renderShadow(modelID) {
const model = this.context.items.ifcModels.find((model) => model.modelID === modelID);
if (!model)
throw new Error('The requested model was not found.');
await this.renderShadowOfMesh(model, `${model.modelID}`);
}
renderShadowOfMesh(model, id = model.uuid) {
if (this.shadows[id])
throw new Error(`There is already a shadow with ID ${id}`);
const { size, center } = this.getSizeAndCenter(model);
const scene = this.context.getScene();
const shadow = this.createShadow(id, size);
this.initializeShadow(model, shadow, scene, center);
this.createPlanes(shadow, size);
this.bakeShadow(model, shadow, scene);
this.context.renderer.postProduction.excludedItems.add(shadow.root);
}
deleteShadow(id) {
const shadow = this.shadows[id];
delete this.shadows[id];
if (!shadow)
throw new Error(`No shadow with ID ${id} was found.`);
disposeMeshRecursively(shadow.root);
disposeMeshRecursively(shadow.blurPlane);
shadow.rt.dispose();
shadow.rtBlur.dispose();
}
createPlanes(currentShadow, size) {
const planeGeometry = new PlaneGeometry(size.x, size.z).rotateX(Math.PI / 2);
this.createBasePlane(currentShadow, planeGeometry);
this.createBlurPlane(currentShadow, planeGeometry);
// this.createGroundColorPlane(currentShadow, planeGeometry);
}
initializeShadow(model, shadow, scene, center) {
this.initializeRoot(model, shadow, scene, center);
this.initializeRenderTargets(shadow);
this.initializeCamera(shadow);
}
bakeShadow(model, shadow, scene) {
const isModelInScene = model.parent !== null && model.parent !== undefined;
if (!isModelInScene)
scene.add(model);
const children = scene.children.filter((obj) => obj !== model && obj !== shadow.root);
for (let i = children.length - 1; i >= 0; i--) {
scene.remove(children[i]);
}
// remove the background
const initialBackground = scene.background;
scene.background = null;
// force the depthMaterial to everything
scene.overrideMaterial = this.depthMaterial;
// render to the render target to get the depths
const renderer = this.context.getRenderer();
renderer.setRenderTarget(shadow.rt);
renderer.render(scene, shadow.camera);
// and reset the override material
scene.overrideMaterial = null;
this.blurShadow(shadow, this.amount);
// a second pass to reduce the artifacts
// (0.4 is the minimum blur amount so that the artifacts are gone)
this.blurShadow(shadow, this.amount * 0.4);
// reset and render the normal scene
renderer.setRenderTarget(null);
scene.background = initialBackground;
for (let i = children.length - 1; i >= 0; i--) {
scene.add(children[i]);
}
if (!isModelInScene)
model.removeFromParent();
}
initializeCamera(shadow) {
shadow.camera.rotation.x = Math.PI / 2; // get the camera to look up
shadow.root.add(shadow.camera);
}
initializeRenderTargets(shadow) {
shadow.rt.texture.generateMipmaps = false;
shadow.rtBlur.texture.generateMipmaps = false;
}
initializeRoot(model, shadow, scene, center) {
const minPosition = this.getLowestYCoordinate(model);
shadow.root.position.set(center.x, minPosition - this.shadowOffset, center.z);
scene.add(shadow.root);
}
// Plane simulating the "ground". This is not needed for BIM models generally
// private createGroundColorPlane(_shadow: Shadow, planeGeometry: BufferGeometry) {
// const fillPlaneMaterial = new MeshBasicMaterial({
// color: this.planeColor,
// opacity: this.opacity,
// transparent: true,
// depthWrite: false,
// clippingPlanes: this.context.getClippingPlanes()
// });
// const fillPlane = new Mesh(planeGeometry, fillPlaneMaterial);
// fillPlane.rotateX(Math.PI);
// fillPlane.renderOrder = -1;
// shadow.root.add(fillPlane);
// }
createBasePlane(shadow, planeGeometry) {
const planeMaterial = this.createPlaneMaterial(shadow);
const plane = new Mesh(planeGeometry, planeMaterial);
// make sure it's rendered after the fillPlane
plane.renderOrder = 2;
shadow.root.add(plane);
// the y from the texture is flipped!
plane.scale.y = -1;
}
createBlurPlane(shadow, planeGeometry) {
shadow.blurPlane.geometry = planeGeometry;
shadow.blurPlane.visible = false;
shadow.root.add(shadow.blurPlane);
}
createPlaneMaterial(shadow) {
return new MeshBasicMaterial({
map: shadow.rt.texture,
opacity: this.opacity,
transparent: true,
depthWrite: false,
clippingPlanes: this.context.getClippingPlanes()
});
}
// like MeshDepthMaterial, but goes from black to transparent
initializeDepthMaterial() {
this.depthMaterial.depthTest = false;
this.depthMaterial.depthWrite = false;
const oldShader = 'gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );';
const newShader = 'gl_FragColor = vec4( vec3( 0.0 ), ( 1.0 - fragCoordZ ) * darkness );';
this.depthMaterial.userData.darkness = { value: this.darkness };
this.depthMaterial.onBeforeCompile = (shader) => {
shader.uniforms.darkness = this.depthMaterial.userData.darkness;
shader.fragmentShader = /* glsl */ `
uniform float darkness;
${shader.fragmentShader.replace(oldShader, newShader)}
`;
};
}
createShadow(id, size) {
this.shadows[id] = {
root: new Group(),
rt: new WebGLRenderTarget(this.resolution, this.resolution),
rtBlur: new WebGLRenderTarget(this.resolution, this.resolution),
blurPlane: new Mesh(),
camera: this.createCamera(size)
};
return this.shadows[id];
}
createCamera(size) {
return new OrthographicCamera(-size.x / 2, size.x / 2, size.z / 2, -size.z / 2, 0, this.cameraHeight);
}
getSizeAndCenter(model) {
const geometry = model.geometry;
geometry.computeBoundingBox();
if (geometry.boundingBox) {
const size = new Vector3();
geometry.boundingBox.getSize(size);
size.x *= 1.5;
size.z *= 1.5;
const center = new Vector3();
geometry.boundingBox.getCenter(center);
return { size, center };
}
throw new Error(`Bounding box could not be computed for the mesh ${model.uuid}`);
}
getLowestYCoordinate(model) {
const indices = model.geometry.index;
const position = model.geometry.attributes.position;
let minPosition = Number.MAX_VALUE;
for (let i = 0; i <= indices.count; i++) {
const current = position.getY(indices.array[i]);
if (current < minPosition)
minPosition = current;
}
return minPosition;
}
blurShadow(shadow, amount) {
const horizontalBlurMaterial = new ShaderMaterial(HorizontalBlurShader);
horizontalBlurMaterial.depthTest = false;
const verticalBlurMaterial = new ShaderMaterial(VerticalBlurShader);
verticalBlurMaterial.depthTest = false;
shadow.blurPlane.visible = true;
// blur horizontally and draw in the renderTargetBlur
shadow.blurPlane.material = horizontalBlurMaterial;
// @ts-ignore
shadow.blurPlane.material.uniforms.tDiffuse.value = shadow.rt.texture;
horizontalBlurMaterial.uniforms.h.value = (amount * 1) / 256;
const renderer = this.context.getRenderer();
renderer.setRenderTarget(shadow.rtBlur);
renderer.render(shadow.blurPlane, shadow.camera);
// blur vertically and draw in the main renderTarget
shadow.blurPlane.material = verticalBlurMaterial;
// @ts-ignore
shadow.blurPlane.material.uniforms.tDiffuse.value = shadow.rtBlur.texture;
verticalBlurMaterial.uniforms.v.value = (amount * 1) / 256;
renderer.setRenderTarget(shadow.rt);
renderer.render(shadow.blurPlane, shadow.camera);
shadow.blurPlane.visible = false;
}
}
//# sourceMappingURL=shadow-dropper.js.map