DEV Community

Dmitrii Tikhomirov
Dmitrii Tikhomirov

Posted on

Three.js for Java devs: J2CL, Closure, Bazel, etc ...

Let's say we really want to write web-based applications on Java using webgl/webxr/webgpu or something. What options do we have?

GWT

The first approach that comes to mind would be using GWT by Google. When it launched 15 years ago, GWT instantly grew very popular and gave rise to many amazing frameworks based on it, like Vaadin, Ext GWT/GXT, Smart GWT, and many others. But it looks like its time has finally come: Google left the GWT project passing it to the community.

If not GWT

If not GWT, there are a few not very active and popular projects like JSweet (a syntax mapper to TypeScript) or TeaVM that’s rather dead according to GitHub activity. Not to worry about our project in the future, we would probably choose something stable and well-maintained.

J2CL

There aren’t many options left, but, luckily for us, we can take a look at J2CL, a Java to Closure style JavaScript transpiler, that is a GWT successor. According to Google, J2CL is widely used in many projects such as Gmail, Inbox, Docs, Slides, and Calendar. At first glance, it’s exactly what we need, but there’s one little problem - it heavily depends on Bazel that differs a lot from our favourite good old Maven.

J2CL is responsible for only one task – to transpile a set of Java classes into a set of JavaScript files. Merging this set of javascripts into one executable script, its optimization and minification is the responsibility of Google’s Closure Compiler.

Google loves Bazel (yeah, a monorepo, reproducible and stable builds and so on), we usually use Maven or Gradle for our projects. Luckily, we can try j2cl-maven-plugin that is developed by Colin Alworth from Vertispan, but for now let’s try the Bazel way.

J2CL with Bazel

First steps with Bazel are pain. What’s good, we can reuse 2 samples provided by the J2CL project, these 2 demos are more than enough to get us started. In short, the most important Bazel rules for us are j2cl_library and j2cl_application that can be used to set groups of files and transpile them into an executable JavaScript file. But for it to work one more step is required: we need to define an entry point for our application. It can be done in 2 ways: we can define it with a handwritten JavaScript script like it works in J2CL demos, or we can generate it with an annotation processor I wrote for this purpose, feel free to take a look at the demo too.

TIP: For more details on j2cl_library and j2cl_application, you could take a look at an amazing article by Thomas Broyer.

OK, so far so good. My favorite IDE is IntelliJ IDEA that has an official Bazel plugin by Google, but if you prefer VS Code you could try a plugin developed by salesforge.

Native APIs

To work with browser APIs from Java we would use Elemental2, which is abstractions for JavaScript APIs, the set of Java wrappers based on the Closure Compiler externs.

Externs are type definitions of, for instance, Browser APIs like HTML elements or WebGL classes, pretty much the same as d.ts, that helps Closure Compiler to recognize the types. By the way, Elemental2 libraries were generated from the built-in Closure Compiler externs using the JsInterop generator. Elemental2 does not cover the whole set of Web APIs, but it’s not too difficult to write missing parts like I did in my research project.

TIP: Peter Donald is working on an impressive project Akasha that aims at creating a unified set of APIs generated from the latest WebIDL specs. It’s kept mostly up-to-date but is incompatible with Elemental2 (as a matter of fact, it’s incompatible with the built-in Closure externs).

JsInterop

So it looks like we have pretty much everything except for the most important part: three.js that we can use from Java. And it’s a little bit of a complex part. To interop with JavaScript we should use J2CL JsInterop API, but there is a little problem: Closure Compiler must be able to recognize types of (most of the) three.js objects. Here we have two options:

  1. Annotate source classes of three.js by Closure type annotations and do some refactoring (Uniforms is pain).
    Advantages: During the optimization phase, Closure Compiler does tree shaking, function inlining and other optimization and other techniques to reduce resulting JavaScript file size. It’s the smallest and fastest option.
    Disadvantages: Adding type annotations to such a huge library like three.js is time-consuming and difficult. From my experience, it needs huge refactoring and is very difficult to maintain. Creating a proof of concept took me two months of hard work.

  2. Use pure three.min.js as is, load it via a JavaScript injection or script tag.
    Advantages: Easy to maintain.
    Disadvantages:
    i. Total code size will be three.min.js + your app code.
    ii. We need externs to generate Java abstractions and make Closure Compiler happy.
    iii. But there is no externs for three.js.

Externs

So I chose the last option: using pure three.min.js. After some googling I found a well-supported d.ts – TypeScript declaration files for three.js by the DefinitelyTyped project.

Well, sounds nice, writing externs from scratch for such a huge project like three.js is an overwhelming task for one person. But there was a little problem: how to convert d.ts to externs? The good news: we can use Tsickle, the project aimed at generating externs from d.ts.

Tsickle

The bad news for me was that after generation the resulting externs didn’t work. It took me a few hours to fix namespace and types issues, and enums had to be replaced by constants, I had no choice, enum support in J2CL is limited now. According to J2CL GitHub issues page, the team is working to improve it in the nearest future.

JsInterop generator

OK, after three.js externs started to work almost as expected (well enough for the time being), I generated Java @JSType based on those externs and finished the demo.

Demo

You can take a look at the demo here:
https://static.treblereel.org/bazel-three4g

The source code is published here:
https://github.com/treblereel/bazel_three_demo

Here is how the working Demo class looks like:

package demo;

import elemental2.dom.DomGlobal;
import org.treblereel.gwt.elemental2.three.BoxGeometry;
import org.treblereel.gwt.elemental2.three.Mesh;
import org.treblereel.gwt.elemental2.three.MeshBasicMaterial;
import org.treblereel.gwt.elemental2.three.MeshBasicMaterialParameters;
import org.treblereel.gwt.elemental2.three.PerspectiveCamera;
import org.treblereel.gwt.elemental2.three.Scene;
import org.treblereel.gwt.elemental2.three.Texture;
import org.treblereel.gwt.elemental2.three.TextureLoader;
import org.treblereel.gwt.elemental2.three.WebGLRenderer;
import org.treblereel.gwt.elemental2.three.WebGLRendererParameters;

public class Demo {

    private PerspectiveCamera camera;
    private Scene scene;
    private Mesh mesh;
    private WebGLRenderer renderer;

    Demo() {
        camera = new PerspectiveCamera( 70, DomGlobal.window.innerWidth / DomGlobal.window.innerHeight, 1, 1000 );
        camera.position.z = 400;
        scene = new Scene();
        Texture texture = new TextureLoader().load("https://threejs.org/examples/textures/crate.gif" );
        MeshBasicMaterialParameters meshBasicMaterialParameters = MeshBasicMaterialParameters.create();
        meshBasicMaterialParameters.setMap(texture);

        BoxGeometry geometry = new BoxGeometry(200, 200, 200 );
        MeshBasicMaterial material = new MeshBasicMaterial(meshBasicMaterialParameters);

        mesh = new Mesh( geometry, material );

        scene.add( mesh );

        WebGLRendererParameters webGLRendererParameters = WebGLRendererParameters.create();
        webGLRendererParameters.setAntialias(true);

        renderer = new WebGLRenderer(webGLRendererParameters);
        renderer.setPixelRatio( DomGlobal.window.devicePixelRatio );
        renderer.setSize( DomGlobal.window.innerWidth, DomGlobal.window.innerHeight );
        DomGlobal.document.body.appendChild( renderer.domElement );

        DomGlobal.window.addEventListener("resize", evt -> onWindowResize(), false);

        animate();
    }

    private void  onWindowResize() {

        camera.aspect = DomGlobal.window.innerWidth / DomGlobal.window.innerHeight;
        camera.updateProjectionMatrix();

        renderer.setSize( DomGlobal.window.innerWidth, DomGlobal.window.innerHeight );
    }

    private void animate() {
        DomGlobal.requestAnimationFrame(timestamp -> animate());
        mesh.rotation.x += 0.005;
        mesh.rotation.y += 0.01;
        renderer.render( scene, camera );
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

  • Bazel with J2CL works great for such tasks, Bazel is very fast!

  • It’s not difficult to maintain a Java three.js wrapper, we only need to keep externs updated.

  • A huge minus is that testing is not opensourced yet, but j2cl-maven-plugin works around it.

  • Source map for debugging doesn’t work (it’s reported at J2CL and Closure GitHub issues page). Perhaps it’s possible to use Vertispan’s Closure Compiler fork that supports a Source map during debug.

  • It doesn’t look like Google is going to popularize J2CL (compared to Flutter). I think they are OK with the current state. I can only assume they use it to support their legacy applications, I hope I’m wrong.

  • Three.js is migrating to ES6. It’s possible to use ES6 classes with J2CL but not in the case of three.js example classes, I hope devs will continue to keep backward compatibility with the pre-ES6 ecosystem.

TIP: Some time ago I ported Quake2 by id Software to J2CL using j2cl-maven-plugin with Quarkus backend, so you can take a look, it rocks!

P.S. It’s my very first article ever, so I'll be happy to hear what I could fix or improve.
P.P.S. The main question: Do we really need it all at all? If yes, I can publish it to Maven as a standalone library.

Top comments (3)

Collapse
 
schubec profile image
schubec • Edited

Thank you for your great article and your efforts for Java/GWT/J2CL!

Do we really need it all at all?

Yes, we need articles like that to show that Java is the best platform to build web apps :-) and your research, which helps others who face problems like you had here to show the correct direction they have to go.

Thank you a thousand times!

Collapse
 
treblereel profile image
Dmitrii Tikhomirov

Thank you, i am going yo write one more article (porting huge app from gwt2 to j2cl-m-p), stay tuned and follow me at twitter/github ... :)

Collapse
 
xamde profile image
Max Völkel

Thanks a lot for this article! Releasing the resulting Three4j2cl library as an example of a j2cl library would be nice. And I would use it :-)