Has anybody tried with three.js and seen any benefits over typescript?

Usually I’m all about “rescript all the things!”, but I think this might be different.

three.js, if you don’t know, its a utility layer over WebGL. If you dig into the examples you’ll notice its mostly about object state and mutating that state. If you do a large projects, you’ll be doing A LOT of object state mutations.

Why I like using rescript is mostly data modeling, pattern matching and the flow that results (whether mutable or immutable), but I’m not sure how much of that happens in a three.js application. Sure you could model something like Sphere(int,int,int), but you aren’t going to use a variant to model a race car. But then again, modeling the behavior of that race car might be a good fit for rescript.

Yes I know rescript can handle three.js objects and setters and getters using bindings, and works well, but at the same time its a clever but weird compromise and if there’s too much of it, especially if it gets too nested, and it might build up like a layer of abstract plaque and make you wish wasn’t there.

Anybody been through this and can give me a “hell no I wouldn’t use rescript for a three.js project again”?

I was miserable when I was using react-three-fiber with rescript. So I changed. Now I do all r3f in JS and wrote bindings so I can control it from normal components in Rescript.

So I have many of these utility functions in JS

// Viewer.js
export function Glass({ args, hex, name, ...props }) {
  return (
    <mesh name={name} scale-z={length} {...props}>
      <boxGeometry args={args} />
      <meshBasicMaterial color={hex} transparent opacity={0.2} />
    </mesh>
  );
}

and I bind them in Rescript where I control the operation

module GlassMesh = {
  @module("./Viewer") @react.component
  external make: (~args: array<float>, ~hex: string, ~name: string) => React.element = "Glass"
}

react-three-fiber moves so fast and threeJS API is so vast.

Thus this is the optimal tradeoff to move fast but with certain degree of safety.

3 Likes

Really this.

Don’t be afraid of JS Interop :slight_smile:

So is that the verdict? Three.js and rescript are a miserable experience?

I’m not so interested in interop with a web framework as I am with writing a complex three.js app in rescript

To be more clear, as you probably know, these are the “ergonomics” of three.js, and in a big app, there would be a lot of this

scene.add( new THREE.AmbientLight( 0xf0f0f0 ) );
const light = new THREE.SpotLight( 0xffffff, 1.5 );
light.position.set( 0, 1500, 200 );
light.angle = Math.PI * 0.2;
light.castShadow = true;
light.shadow.camera.near = 200;
light.shadow.camera.far = 2000;
light.shadow.bias = - 0.000222;
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
scene.add( light );

thats a perfect match for typescript, but if it was converted to rescript, I’m not even sure what it would look like, or if it would be so contorted it would barely be rescript anymore, just solving binding puzzles.

And your application is not just a WebGL demo, right? The domain is still modeled in ReScript, and three.js is “just” an unusual view layer, that is, 3D graphics instead of React components.

What I found works best for my app based on ReScript and three.js is having a dedicated layer that converts my domain objects (a drawing intended to be laser-cut) to three.js scene. The domain has no dependency on three.js.

The bindings are not puzzles but can look a bit verbose. Here’s some random snippet from the conversion layer to give an idea:

let bboxToSphere = bbox =>
  Three.Sphere.make(Vector3.fromVector2(bbox->BBox.center, 0.0), bbox->BBox.diagonalLength /. 2.0)

let makeCurveWireframeGeometry = (
  curve: 'curve,
  lengthInUnits: 'curve => float,
  tesselate: ('curve, int) => array<Vector2.t>,
  ~tesselationStep: float,
  ~thicknessInUnits: float,
  ~bbox: BBox.t,
) => {
  let tesselationDivisions = (lengthInUnits(curve) /. tesselationStep)->Js.Math.ceil_int
  let facePoints = tesselate(curve, tesselationDivisions)
  let nFacePoints = facePoints->Array.length

  let edgeEvery = 4 // only 1 of `edgeEvery` will be used to draw an edge
  let edgePoints =
    facePoints->Array.keepWithIndex((_, index) =>
      mod(index, edgeEvery) == 0 || index == nFacePoints - 1
    )

  let zfar = -.thicknessInUnits
  let bsphere = bboxToSphere(bbox)

  let frontFaceGeo = Three.BufferGeometry.make()
  frontFaceGeo->Three.BufferGeometry.setFromPoints2d(
    facePoints->ArrayX.pairwise->ArrayX.flattenTuples2,
  )
  // Deal with https://github.com/mrdoob/three.js/issues/6288
  frontFaceGeo->Three.BufferGeometry.setBoundingSphere(bsphere)

  let backFaceGeo = frontFaceGeo->Three.BufferGeometry.clone
  backFaceGeo->Three.BufferGeometry.translate(0.0, 0.0, zfar)

  let edgesGeo = Three.BufferGeometry.make()

  edgesGeo->Three.BufferGeometry.setFromPoints(
    edgePoints
    ->Array.map(v2 => [Vector3.fromVector2(v2, 0.0), Vector3.fromVector2(v2, zfar)])
    ->Array.concatMany,
  )

  // Deal with https://github.com/mrdoob/three.js/issues/6288
  edgesGeo->Three.BufferGeometry.setBoundingSphere(bsphere)

  Three.BufferGeometryUtils.mergeBufferGeometries(
    [frontFaceGeo, backFaceGeo, edgesGeo],
    ~useGroups=false,
    (),
  )
}

let makePathGeometry = (
  path: Shape.Path.t,
  ~tesselationStep: float,
  ~thicknessInUnits: float,
  ~bbox: BBox.t,
) => {
  let makeWire = makeCurveWireframeGeometry(~tesselationStep, ~thicknessInUnits, ~bbox)
  switch path {
  | Circle(circle) => makeWire(circle, Curves.Circle.length, Curves.Circle.tesselate)
  | Ellipse(ellipse) => makeWire(ellipse, Curves.Ellipse.length, Curves.Ellipse.tesselate)
  | Poly(segments) =>
    let segmentsGeo = segments->Array.map(s =>
      switch s {
      | Line(line) => makeWire(line, Curves.Line.length, Curves.Line.tesselate)
      | Arc(arc) => makeWire(arc, Curves.Arc.length, Curves.Arc.tesselate)
      | Spline(spline) => makeWire(spline, Curves.Spline.length, Curves.Spline.tesselate)
      | QuadraticBezier(bezier) =>
        makeWire(bezier, Curves.QuadraticBezier.length, Curves.QuadraticBezier.tesselate)
      | CubicBezier(bezier) =>
        makeWire(bezier, Curves.CubicBezier.length, Curves.CubicBezier.tesselate)
      }
    )
    Three.BufferGeometryUtils.mergeBufferGeometries(segmentsGeo, ())
  }
}

let makeShapeGeometry = (
  shape: Shape.t,
  ~tesselationStep: float,
  ~thicknessInUnits: float,
  ~bbox: BBox.t,
) => {
  let makeWire = makePathGeometry(_, ~tesselationStep, ~thicknessInUnits, ~bbox)
  Three.BufferGeometryUtils.mergeBufferGeometries(shape->Shape.allPaths->Array.map(makeWire), ())
}
1 Like

I’m afraid of having to use Obj.magic

Well if you aren’t using r3f then makes sense to do the bindings. I’m not that good at binding nested objects so for me it would look like a rabbit hole. But you might be better at it.

You can also use GenType to convert ThreeJS Typescript bindings to Rescript.

AFAIK it’s only supported the other way around: GenType can export ts types based on your rescript code.


I’m not very familiar with three.js at all and I don’t have any eperience of using it with resccript.
But that’s how I imagine your given js code to look in rescript:

// JS
scene.add( new THREE.AmbientLight( 0xf0f0f0 ) );
const light = new THREE.SpotLight( 0xffffff, 1.5 );
light.position.set( 0, 1500, 200 );
light.angle = Math.PI * 0.2;
light.castShadow = true;
light.shadow.camera.near = 200;
light.shadow.camera.far = 2000;
light.shadow.bias = - 0.000222;
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
scene.add( light );
// rescript
scene->Scene.add(Three.AmbientLight.make("0xf0f0f0"))
let light = Three.SpotLight.make("0xffffff", 1.5)
{
  open Light
  light->setPosition(0, 1500, 200)
  light->setAngle(Js.Math.Pi * 0.2)
  light->setCastShadow(true)
  light->shadow->camera->setShadowCameraNew(200)
  light->shadow->camera->setShadowCameraFar(2000)
  light->shadow->setShadowBias(-0.000222)
  light->shadow->mapSize->setShadowMapSizeWidth(1024)
  light->shadow->mapSize->setShadowMapSizeHeight(1024)
  scene->Scene.add(light)
}

I haven’t written any actual bindings but that’s how I’d imagine a rescript api for three.js. That’s actually not too bad.

Although I have to admit, if you barely have any actual logic in your application the time spent to write bindings for three.js could probably be better used by just using typescript and build on top of already existing types.

On the other hand, if you have more logic and data to work with in your application (like @nkrkv ) I think it would be a great match.