Havok Error: Failed to construct 'URL': Invalid URL

Hi,

i am new to Babylon.js and building my first 3D Configurator with it. I want to use a Physics Engine to have Collisions on my Meshes.

In the Background i just use esbuild to bundle all my files i need. So nothing fancy to it.

My Script to bundle the files looks like this:

"esbuild:dev": "esbuild ./.src/js/configurator.js --bundle --minify --outfile=./assets/tecdip-configurator.js --watch"

The Imports inside my configurator.js looks like this:

import "@babylonjs/loaders/glTF";
import "@babylonjs/core/Physics/physicsEngine";
import HavokPhysics from '@babylonjs/havok';
import { HavokPlugin, SceneLoader, Mesh, Engine, Scalar, CreatePlane, Scene, Tools, ShadowGenerator, SpotLight, StandardMaterial, CreateGround, Vector3, Color3, ArcRotateCamera, ExecuteCodeAction, ActionManager, PBRMaterial, Texture, HighlightLayer, PointerDragBehavior, CreateSphere } from "@babylonjs/core";
import { Button, AdvancedDynamicTexture } from "@babylonjs/gui";
import { Inspector } from '@babylonjs/inspector';

This Imports work perfectly fine expected for the Havok.

I use Havok like this:

const hk = await HavokPhysics();
this.scene.enablePhysics(new Vector3(0, -10, 0), new HavokPlugin(true, hk));

if i want to bundle the havok with the wasm it prints me in the Browser:

Uncaught (in promise) TypeError: Failed to construct 'URL': Invalid URL
    at tecdip-configurator.js:30581:1075793
    at H0e.setupScene (tecdip-configurator.js:30604:26002)
    at new H0e (tecdip-configurator.js:30604:22233)
    at tecdip-configurator.js:30604:26644
    at tecdip-configurator.js:30604:26678

is this related to esbuild because it cant handle wasm files? i wonder, because webpack does internal almost the same or not?

Thanks for any help!

cc @RaananW

Would you be able to share your configuration?

I believe it is related to the URL being generated internally to find the wasm. Unless you provide either an arraybuffer of your wasm or a different URL, the .js Havok import will attempt to construct and dnownload the wasm on its own.

A reproduction would be helpful to be able to help

@RaananW

i shared you my whole code. Sadly i cant share you the glb files i used, because of an NDA.

Inside the setupScene() you can find the Physics code

import "@babylonjs/loaders/glTF";
import "@babylonjs/core/Physics/physicsEngine";
import HavokPhysics from '@babylonjs/havok';
import { HavokPlugin, SceneLoader, Mesh, Engine, Scalar, CreatePlane, Scene, Tools, ShadowGenerator, SpotLight, StandardMaterial, CreateGround, Vector3, Color3, ArcRotateCamera, ExecuteCodeAction, ActionManager, PBRMaterial, Texture, HighlightLayer, PointerDragBehavior, CreateSphere } from "@babylonjs/core";
import { Button, AdvancedDynamicTexture } from "@babylonjs/gui";
import { Inspector } from '@babylonjs/inspector';

class Configurator extends HTMLElement {
  constructor() {
    super();

    this.html = document.querySelector("html");
    this.canvas = this.querySelector("canvas");
    this.engine = new Engine(this.canvas, true, { stencil: true });
    this.scene = new Scene(this.engine);
    this.fullScreenUi = AdvancedDynamicTexture.CreateFullscreenUI("configuratorUi");
    this.lights = [];
    this.materals = [];
    this.walls = [];
    this.modules = [];
    this.floor = null;
    this.camera = null;

    this.setupScene();
  }

  setupFloorMaterial() {
    const floorMaterial = new PBRMaterial("floor_material", this.scene);
    const albedoTexture = new Texture(livom.configurator.floorMaterial.map, this.scene);
    const bumpTexture = new Texture(livom.configurator.floorMaterial.normalMap, this.scene);
    const metallicTexture = new Texture(livom.configurator.floorMaterial.roughnessMap, this.scene);

    albedoTexture.uScale = 1.5;
    albedoTexture.vScale = 1.5;
    bumpTexture.uScale = 1.5;
    bumpTexture.vScale = 1.5;
    metallicTexture.uScale = 1.5;
    metallicTexture.vScale = 1.5;
    floorMaterial.albedoTexture = albedoTexture;
    floorMaterial.bumpTexture = bumpTexture;
    floorMaterial.metallicTexture = metallicTexture;

    this.materals.push(floorMaterial);
    return floorMaterial;
  }

  setupWallMaterial() {
    let wallMaterial = new StandardMaterial("wall_material", this.scene);
    wallMaterial.diffuseColor = Color3.White();
    wallMaterial.emissiveColor = Color3.White();
    wallMaterial.backFaceCulling = true;

    this.materals.push(wallMaterial);
  }

  setupFloor() {
    this.floor = CreateGround("ground", { width: 6, height: 6, updatable: true }, this.scene);
    this.floor.receiveShadows = true;
    this.floor.material = this.setupFloorMaterial();
  }

  setupCamera() {
    const camera = new ArcRotateCamera("camera", 1, 1, 10, new Vector3(0.0, 0.5, 0.0), this.scene);
    camera.keysUp.push(87);
    camera.keysLeft.push(65);
    camera.keysDown.push(83);
    camera.keysRight.push(68);
    camera.attachControl(this.canvas, true);
    camera.ellipsoid = new Vector3(0.5, 1.0, 0.5);

    this.camera = camera;
  }

  setupLight() {
    const spotLight = new SpotLight("SpotLight", new Vector3(0, 5, 0), new Vector3(0, -1, 0), Math.PI, 10, this.scene);
    spotLight.intensity = 100;
    spotLight.shadowEnabled = true;
    this.lights.push(spotLight);
    return spotLight;
  }

  setupWalls() {
    const wallHeight = 3;
    const wallLength = 6;

    const walls = [
      {
        position: new Vector3(0, wallHeight / 2, wallLength / 2),
        rotationY: Tools.ToRadians(180)
      },
      {
        position: new Vector3(0, wallHeight / 2, (wallLength / 2) * -1),
        rotationY: Tools.ToRadians(0)
      },
      {
        position: new Vector3(wallLength / 2, wallHeight / 2, 0),
        rotationY: Tools.ToRadians(-90)
      },
      {
        position: new Vector3((wallLength / 2) * -1, wallHeight / 2, 0),
        rotationY: Tools.ToRadians(90)
      }
    ]

    for (const [index, wall] of walls.entries()) {
      let wallPlane = CreatePlane("wall_" + index, { height: wallHeight, width: wallLength, sideOrientation: Mesh.BACKSIDE }, this.scene);
      wallPlane.position = wall.position;
      wallPlane.rotation.y = wall.rotationY;
      wallPlane.checkCollisions = false;
      wallPlane.isPickable = false;
      wallPlane.receiveShadows = true;

      this.walls.push(wallPlane);
    }
  }

  setupHighlightLayer () {
    return new HighlightLayer("highlightLayer", this.scene);
  }

  shadowGenerator(light) {
    const shadowGenerator = new ShadowGenerator(4096, light);
    return shadowGenerator;
  }

  async importModule(moduleContainer, moduleObject) {
    const url = new URL("https:" + moduleObject.src);
    const obj = await SceneLoader.ImportMeshAsync("", url.origin + url.pathname.split("/LI")[0] + "/", "LI" + url.pathname.split("/LI")[1] + "?" + url.searchParams, this.scene);
    const minMax = Mesh.MinMax(obj.meshes.filter((mesh) => mesh.id !== "__root__" && mesh.id !== "bottom"));
    const size = {
      width: minMax.max.x - minMax.min.x,
      height: minMax.max.y - minMax.min.y,
      depth: minMax.max.z - minMax.min.z
    }

    obj.size = size;
    obj.id = moduleObject.id;
    obj.name = moduleContainer.modell;

    this.modules.push(obj);
    return obj;
  }

  enableDragOfModules (obj, highlightLayer) {
    const getAllMeshesForHightlight = obj.meshes.filter((mesh) => mesh.id !== "__root__");

    const dragBehavior = new PointerDragBehavior({
      dragPlaneNormal: new Vector3(0, 1, 0),
    });

    dragBehavior.useObjectOrientationForDragging = false;

    dragBehavior.onDragStartObservable.add((e) => {
      for (const mesh of getAllMeshesForHightlight) {
        highlightLayer.addMesh(mesh, Color3.Green());
      }
    });

    dragBehavior.onDragObservable.add((e) => { });

    dragBehavior.validateDrag = (targetPosition) => {
      const bounds = this.floor.getBoundingInfo().boundingBox;

      targetPosition.x = Scalar.Clamp(targetPosition.x, bounds.minimum.x, bounds.maximum.x);
      targetPosition.y = Scalar.Clamp(targetPosition.y, bounds.minimum.y, bounds.maximum.y);
      targetPosition.z = Scalar.Clamp(targetPosition.z, bounds.minimum.z, bounds.maximum.z);
      return true;
    }

    dragBehavior.onDragEndObservable.add(() => {
      for (const mesh of getAllMeshesForHightlight) {
        highlightLayer.removeMesh(mesh, Color3.Green());
      }
    });

    dragBehavior.attach(obj.meshes[0]);
  }

  setupShadowCaster (shadowGenerator) {
    const rootMeshes = this.scene.getMeshesById("__root__");

    for (const rootMesh of rootMeshes) {
      let meshes = rootMesh.getChildMeshes();

      for (const mesh of meshes) {
        shadowGenerator.addShadowCaster(mesh);
      }
    }
  }

  createHotspot(mesh, positions, key) {
    const hotspot = new CreatePlane(
      `hotspot_${key}_${mesh.name}`, 
      {
        sideOrientation: Mesh.BACKSIDE,
      },
      this.scene
    );
    hotspot.position = positions[key];
    hotspot.parent = mesh; // Parent the hotspot to the main mesh

    const GUI = AdvancedDynamicTexture.CreateForMesh(hotspot);
    const button = Button.CreateSimpleButton("but", "Click Me");
    button.width = 1;
    button.height = 0.4;
    button.color = "white";
    button.fontSize = 50;
    button.background = "green";
    button.onPointerUpObservable.add(function() {
      alert("you did it!");
    });
    GUI.addControl(button);
    console.log(button);
  }

  createHotspots(mesh) {
    if (mesh.id === "legs" || mesh.id === "bottom" || mesh.id === "__root__") return;
    const boundingBox = mesh.getBoundingInfo().boundingBox;
    let positions = {
      top: new Vector3(boundingBox.center.x, boundingBox.maximumWorld.y + 0.01, boundingBox.center.z),
      bottom: new Vector3(boundingBox.center.x, boundingBox.minimumWorld.y - 0.01, boundingBox.center.z),
      left: new Vector3(boundingBox.minimumWorld.x - 0.01, boundingBox.center.y, boundingBox.center.z),
      right: new Vector3(boundingBox.maximumWorld.x + 0.01, boundingBox.center.y, boundingBox.center.z),
      front: new Vector3(boundingBox.center.x, boundingBox.center.y, boundingBox.maximumWorld.z + 0.01),
      back: new Vector3(boundingBox.center.x, boundingBox.center.y, boundingBox.minimumWorld.z - 0.01)
    };

    if (mesh.id === "cushion" || mesh.id === "back_lean") {
      positions = {
        top: new Vector3(boundingBox.center.x, boundingBox.maximumWorld.y + 0.01, boundingBox.center.z),
      }
    }

    // Create hotspot meshes
    Object.keys(positions).forEach(key => {
      this.createHotspot(mesh, positions, key);
    });
  }

  async setupScene() {
    const hk = await HavokPhysics();
    console.log(hk);
    this.scene.enablePhysics(new Vector3(0, -10, 0), new HavokPlugin(true, hk));
    this.html.style.overflow = "hidden";
    this.setupFloor();
    this.setupCamera();
    this.setupLight();
    this.setupWalls();
    const shadowGenerator = this.shadowGenerator(this.lights[0]);
    const highlightLayer = this.setupHighlightLayer();

    for (const moduleContainer of livom.configurator.modules) {
      for (const moduleObject of moduleContainer.modules) {
        const obj = await this.importModule(moduleContainer, moduleObject);
        this.enableDragOfModules(obj, highlightLayer);
  
        for (const mesh of obj.meshes) {
          this.createHotspots(mesh);
        } 
      }
    }

    console.log(this.modules);

    this.setupShadowCaster(shadowGenerator);

    this.engine.runRenderLoop(() => {
      this.scene.render();
    });

    window.addEventListener('resize', () => {
      this.engine.resize();
    });

    Inspector.Show(this.scene, {
      embedMode: true
    });
  }
}

customElements.define('configurator-canvas', Configurator);

I am less interested in the assets and more in the project setup and what can be changed to make it work. I got the code, but this will just take me longer to prepare. Of course, I can assume that adding the custom element to a standard HTMl element will reproduce this, but then again, it might be a little deeper than that. This is why I usually ask for a project reproduction - some code I can simply take, run npm install, run npm start, and see the error.

I will Setup a small Repo this evening.

2 Likes

@tecdip I ran into the same problem, that the HavokPhysics instantiation was failing becaues of invalid URL (for the WASM file). I ended-up manually copying the HavokPhysics.wasm file into my output folder and then point the HavokPhysics instance to it like:

const havokInstance = await HavokPhysics({
    locateFile: () => {
      return 'path/to/your/HavokPhysics.wasm'
    },
  })
  const havokPlugin = new HavokPlugin(true, havokInstance)
  scene.enablePhysics(gravity, havokPlugin)

I found this workaround here Unable to load Havok plugin (error while loading .wasm file from browser) - #45 by dap