0

When I load up my IFC.js viewer and add my models to the scene, there are some walls and assets that seem to bug out a little whenever I move and navigate around. This occurs on most any file I attempt to view, including all of the example files provided by IFC.js's Github, so I do not believe the issue lies with the files being rendered.

Below you can see the same room with just slight differences of where my camera is positioned. As you can tell, there seems to be some issue rendering the wall as the pixelated squares jump all over the surface.

Example 1 Example 2 Example 3

Here is the code that I use for everything regarding creation of an IFC viewer in my typescript Vue Project:

/* eslint-disable  @typescript-eslint/no-non-null-assertion */
import {
  AmbientLight,
  BufferAttribute,
  BufferGeometry,
  Clock,
  Color,
  DirectionalLight,
  Material,
  Mesh,
  MeshLambertMaterial,
  PerspectiveCamera,
  Raycaster,
  Scene,
  Vector2,
  Vector3,
  WebGLRenderer,
} from "three";
import type { Intersection } from "three";
import * as THREE from "three";
import CameraControls from "camera-controls";
import { IFCLoader } from "web-ifc-three/IFCLoader";
import {
  acceleratedRaycast,
  computeBoundsTree,
  disposeBoundsTree,
} from "three-mesh-bvh";
import type { IFCManager } from "web-ifc-three/IFC/components/IFCManager";
import type { IFCModel } from "web-ifc-three/IFC/components/IFCModel";
import { DEG2RAD } from "three/src/math/MathUtils";
import type { Subset } from "web-ifc-three/IFC/components/subsets/SubsetManager";
import type { SecureSiteMicroservice } from "@/services/SiteMicroservice";
import { getConfig } from "@/config";
import {
  fetchingConsoleLog,
  successConsoleLog,
} from "@/functions/ConsoleLogSupport";

export const BASE_CAMERA_POSITION = new Vector3(110, 30, 15);
export const WARNING_MATERIAL = new MeshLambertMaterial({
  transparent: true,
  opacity: 0.6,
  color: 0xffff00,
  depthTest: false,
});
export const ERROR_MATERIAL = new MeshLambertMaterial({
  transparent: true,
  opacity: 0.6,
  color: 0x8b0000,
  depthTest: false,
});

interface IfcScene {
  /**
   * The HTML canvas element in which the scene will be rendered
   */
  threeCanvas: HTMLElement;

  /**
   * The ThreeJS Scene object
   */
  scene: Scene;

  /**
   * The id of the main model loaded into the scene
   */
  modelId: number;

  /**
   * Whether the scene has finished loading the addition of the model
   */
  isLoading: boolean;

  /**
   * The ThreeJS Camera
   */
  camera: PerspectiveCamera;

  /**
   * The offset width of the threeCanvas HTMLElement property
   */
  width: number;

  /**
   * The offset width of the threeCanvas HTMLElement property
   */
  height: number;

  /**
   * ThreeJS renderer
   */
  renderer: WebGLRenderer;

  /**
   * The IFCModels loaded into the scene
   */
  ifcModels: IFCModel[];

  /**
   * The highlight Subsets currently loaded within the scene
   */
  highlights: Subset[];

  /**
   * The ThreeJS raycaster
   */
  raycaster: Raycaster;

  /**
   * The ThreeJS mouse
   */
  mouse: Vector2;
}

export class IfcViewer {
  scene: IfcScene;
  controls: CameraControls;
  clock: Clock;
  loader: IFCLoader;
  manager: IFCManager;
  secureSiteMicroservice: SecureSiteMicroservice;

  constructor(
    threeCanvas: HTMLElement,
    secureSiteMicroservice: SecureSiteMicroservice
  ) {
    this.scene = this.setupScene(threeCanvas);
    this.secureSiteMicroservice = secureSiteMicroservice;
    this.loader = new IFCLoader();
    this.loader.setRequestHeader({
      authorization: secureSiteMicroservice.getAuthorizationHeader(),
    });
    this.manager = this.loader.ifcManager;

    CameraControls.install({ THREE: THREE });
    this.controls = new CameraControls(
      this.scene.camera,
      this.scene.renderer.domElement
    );
    this.clock = new Clock();

    this.setupLights();
    this.setupWindowResize();
    this.setupAnimation();
    this.setupControls().then();
    this.configureManager().then();
  }

  private async configureManager() {
    // TODO: Fix multithreading, after serving from backend loadAsync() is flaky
    // await this.manager.useWebWorkers(true, "../IFCWorker.js");
    await this.manager.setWasmPath("../../../");

    await this.manager.applyWebIfcConfig({
      COORDINATE_TO_ORIGIN: true,
      USE_FAST_BOOLS: true,
    });
    await this.manager.setupThreeMeshBVH(
      computeBoundsTree,
      disposeBoundsTree,
      acceleratedRaycast
    );
  }

  private setupScene(threeCanvas: HTMLElement): IfcScene {
    const width = threeCanvas.offsetWidth;
    const height = threeCanvas.offsetHeight;

    const raycaster = new Raycaster();
    raycaster.firstHitOnly = true;
    const mouse = new Vector2();

    const camera = new PerspectiveCamera(60, width / height, 1, 1000);
    const renderer = new WebGLRenderer({
      antialias: true,
      canvas: threeCanvas,
    });
    renderer.setSize(width, height);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.xr.enabled = true;
    renderer.setAnimationLoop(() => {
      renderer.render(this.scene.scene, this.scene.camera);
    });

    const scene = new Scene();

    const ifcScene: IfcScene = {
      threeCanvas: threeCanvas,
      scene,
      modelId: 0,
      isLoading: true,
      width,
      height,
      camera,
      renderer,
      ifcModels: [],
      highlights: [],
      raycaster,
      mouse,
    };

    ifcScene.scene.background = new Color(0x6c757d);
    return ifcScene;
  }

  private setupAnimation() {
    const delta = this.clock.getDelta();
    const hasControlsUpdated = this.controls.update(delta);

    requestAnimationFrame(() => this.setupAnimation());

    if (hasControlsUpdated) {
      this.scene.renderer.render(this.scene.scene, this.scene.camera);
    }
  }

  private calculateWidthHeight() {
    this.scene.width = this.scene.threeCanvas.offsetWidth;
    this.scene.height = this.scene.threeCanvas.offsetHeight;
  }

  private setupLights() {
    const directionalLight1 = new DirectionalLight(0xffeeff, 0.8);
    directionalLight1.position.set(1, 1, 1);
    this.scene.scene.add(directionalLight1);

    const directionalLight2 = new DirectionalLight(0xffffff, 0.8);
    directionalLight2.position.set(-1, 0.5, -1);
    this.scene.scene.add(directionalLight2);

    const ambientLight = new AmbientLight(0xffffee, 0.25);
    this.scene.scene.add(ambientLight);
  }

  private setupWindowResize() {
    window.addEventListener("resize", () => {
      this.calculateWidthHeight();
      this.scene.camera.aspect = this.scene.width / this.scene.height;
      this.scene.camera.updateProjectionMatrix();
      this.scene.renderer.setSize(window.innerWidth, window.innerHeight);
    });
  }

  private async setupControls() {
    this.controls.mouseButtons.middle = CameraControls.ACTION.DOLLY;
    await this.controls.setLookAt(
      BASE_CAMERA_POSITION.x,
      BASE_CAMERA_POSITION.y,
      BASE_CAMERA_POSITION.z,
      0,
      0,
      0
    );
  }

  async load(ifcFilePath: string) {
    const config = getConfig();
    const ifcFileUrl = `${config.siteMicroserviceBaseUrl}/api/v1/secured/files/${ifcFilePath}`;
    fetchingConsoleLog(`Attempting to fetch ${ifcFileUrl}`);
    const file = await this.loader.loadAsync(ifcFileUrl);
    successConsoleLog(`Successfully loaded ${ifcFileUrl}`);
    this.addToScene(file);
  }

  addToScene(model: IFCModel): Scene {
    const updatedScene = this.scene.scene.add(model);
    this.scene.ifcModels.push(model);
    return updatedScene;
  }

  addHighlight(assetId: number, type: string) {
    let material;

    switch (type) {
      case "error":
        material = ERROR_MATERIAL;
        break;
      case "warning":
        material = WARNING_MATERIAL;
        break;
    }

    const highlight = this.manager.createSubset({
      modelID: this.scene.modelId,
      ids: [assetId],
      material,
      scene: this.scene.scene,
      removePrevious: true,
      customID: `${assetId}`,
    });

    this.scene.highlights.push(highlight);
  }

  removeHighlight(assetId: number) {
    this.manager
      .getSubset(this.scene.modelId, ERROR_MATERIAL, assetId.toString())
      .removeFromParent();
  }

  retrieveFirstIntersection(event: MouseEvent): Intersection {
    const ifcScene = this.scene;

    const bounds = ifcScene.threeCanvas.getBoundingClientRect();
    const x1 = event.clientX - bounds.left;
    const x2 = bounds.right - bounds.left;
    ifcScene.mouse.x = (x1 / x2) * 2 - 1;

    const y1 = event.clientY - bounds.top;
    const y2 = bounds.bottom - bounds.top;
    ifcScene.mouse.y = -(y1 / y2) * 2 + 1;

    ifcScene.raycaster.setFromCamera(ifcScene.mouse, ifcScene.camera);

    return ifcScene.raycaster.intersectObjects(ifcScene.ifcModels)?.[0];
  }

  getAssetId(geometry: BufferGeometry, faceIndex: number): number {
    return this.manager.getExpressId(geometry, faceIndex);
  }

  determineAssetGeometricCenter(assetId: number): Vector3 {
    const customID = assetId.toString();
    const coordinates = [];
    const expressIDs = [];
    const newIndices = [];

    const alreadySaved = new Map();

    // Get the subset for the asset
    const subset = this.manager.createSubset({
      ids: [assetId],
      modelID: this.scene.modelId,
      removePrevious: true,
      customID,
    });

    // Subsets have their own index, but share the BufferAttributes
    // with the original geometry, so we need to rebuild a new
    // geometry with this index

    const positionAttr = subset.geometry.attributes.position;
    const expressIDAttr = subset.geometry.attributes.expressID;

    const newGroups = subset.geometry.groups.filter(
      (group) => group.count !== 0
    );

    const newMaterials = [] as Material[];
    const prevMaterials = subset.material as Material[];
    let newMaterialIndex = 0;

    newGroups.forEach((group) => {
      newMaterials.push(prevMaterials[group.materialIndex!]);
      group.materialIndex = newMaterialIndex++;
    });

    let newIndex = 0;
    for (let i = 0; i < subset.geometry.index!.count; i++) {
      const index = subset.geometry.index!.array[i];

      if (!alreadySaved.has(index)) {
        coordinates.push(positionAttr.array[3 * index]);
        coordinates.push(positionAttr.array[3 * index + 1]);
        coordinates.push(positionAttr.array[3 * index + 2]);

        expressIDs.push(expressIDAttr.getX(index));
        alreadySaved.set(index, newIndex++);
      }

      const saved = alreadySaved.get(index);
      newIndices.push(saved);
    }

    const geometryToExport = new BufferGeometry();
    const newVerticesAttr = new BufferAttribute(
      Float32Array.from(coordinates),
      3
    );
    const newExpressIDAttr = new BufferAttribute(
      Uint32Array.from(expressIDs),
      1
    );

    geometryToExport.setAttribute("position", newVerticesAttr);
    geometryToExport.setAttribute("expressID", newExpressIDAttr);
    geometryToExport.setIndex(newIndices);
    geometryToExport.groups = newGroups;
    geometryToExport.computeVertexNormals();

    const mesh = new Mesh(geometryToExport, newMaterials);
    mesh.geometry.computeBoundingBox();

    subset.removeFromParent();
    return mesh.geometry.boundingBox!.getCenter(new Vector3());
  }

  async getItemProperties(assetId: number): Promise<unknown> {
    return this.manager.getItemProperties(this.scene.modelId, assetId);
  }

  async targetAsset(targetVector: Vector3, cameraPosition?: Vector3) {
    cameraPosition ||= this.scene.camera.position;
    await this.controls.setLookAt(
      cameraPosition.x,
      cameraPosition.y,
      cameraPosition.z,
      targetVector.x,
      targetVector.y,
      targetVector.z,
      true
    );
  }

  async move(direction: string, distance = 5) {
    let x = 0;
    let y = 0;

    switch (direction) {
      case "up": {
        y = -distance;
        break;
      }
      case "down": {
        y = distance;
        break;
      }
      case "right": {
        x = distance;
        break;
      }
      case "left": {
        x = -distance;
        break;
      }
    }
    await this.controls.truck(x, y, true);
  }

  async zoom(direction: string, distance = 5) {
    switch (direction) {
      case "out":
        distance = -distance;
        break;
    }

    await this.controls.dolly(distance, true);
  }

  async rotate(direction: string, degrees = 30) {
    let x = 0;
    let y = 0;

    switch (direction) {
      case "right": {
        x = degrees * DEG2RAD;
        break;
      }
      case "left": {
        x = -degrees * DEG2RAD;
        break;
      }
      case "down": {
        y = degrees * DEG2RAD;
        break;
      }
      case "up": {
        y = -degrees * DEG2RAD;
        break;
      }
    }
    await this.controls.rotate(x, y, true);
  }
}

I followed all instructions for configuring the viewer and am expecting this rendering issue to not happen as the example on IFC.js's website do not appear to have this pixelation issue.

0 Answers0