Skip to content

Quick Start. TeaVM

zeganstyl edited this page Jul 17, 2020 · 5 revisions

Table of Contents

  1. Setup IntelliJ IDEA
  2. Initial Gradle setup
  3. Writing a code
  4. Setup Gradle to work with TeaVM

Setup IntelliJ IDEA

Create new project in IDEA: File > New > Project

Choose Gradle

Choose Kotlin/JVM as framework

Write project name

Setup Gradle

Add jcenter to repositories:

repositories {
    jcenter()
}

To create web-application, add thelema-teavm to dependecies:

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    implementation "org.ksdfv.thelema:thelema-teavm:0.3.0"
}

Writing a code

In kotlin directory src/main/kotlin create package and name it for somthing like "com.example"

In this package create object Main. Type main, and IDEA will suggest you create main function, accept it. Code will looks like this:

package com.example

object Main {
    @JvmStatic
    fun main(args: Array<String>) {
        
    }
}

To initialize engine, write:

val app = TeaVMApp()

We will create a basic application using built-in tools, but we will still create the shader manually. Write in main function:

@Language("GLSL")
val shader = Shader(
    vertCode = """
attribute vec3 aPosition;
varying vec3 vPosition;
uniform mat4 viewProj;
void main() {
    vPosition = aPosition;
    gl_Position = viewProj * vec4(aPosition, 1.0);
}""",
    fragCode = """
varying vec3 vPosition;
void main() {
    gl_FragColor = vec4(vPosition, 1.0);
}"""
)

In the shader, we only use one aPosition attribute. The name of this attribute used by default in Thelema.

The value of this attribute passed to the fragment shader via varying variable.

We also pass the camera matrix and multiply by the position, so that the object positioned relative to the camera and its vertex correctly projected onto the screen.

Now we need to create meshes, and a function that will be called in the render loop.

val box = BoxMeshBuilder().build()

val control = OrbitCameraControl()
control.listenToMouse()

GL.glClearColor(0f, 0f, 0f, 1f)

GL.isDepthTestEnabled = true

We use the built-in tool BoxMeshBuilder to generate a mesh box. Create a camera controller OrbitCameraControl and bind it to mouse events, so that we can rotate and move the camera.

Set background color and enable depth test.

Now let's define how rendering must work.

GL.render {
    GL.glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)

    control.update(APP.deltaTime)
    ActiveCamera.update()

    shader.bind()
    shader["viewProj"] = ActiveCamera.viewProjectionMatrix
    box.render(shader)
}

Сlear the screen with a given color.

Here we have to update the state of the camera controller and correspondingly update the camera matrix (the camera updated separately from the controller).

We transfer necessary data to the shader, in particular the camera matrix.

And finally, render our mesh with shader.

Start render loop:

app.startLoop()

Camera controls:

  • rotate by holding down the Middle mouse button
  • move while holding Shift + Middle mouse button

Full source code

We also need a web page for running our application. Create index.html file in directory src/main/resources and add the following code there:

<!DOCTYPE html>
<html>
<head>
    <title>TeaVM example</title>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
    <script type="text/javascript" charset="utf-8" src="classes.js"></script>
</head>
<body onload="main()">
<canvas id="canvas" width="1280" height="720"></canvas>
</body>
</html>

Setup Gradle to work with TeaVM

To compile code to javascript, we will use the TeaVM CLI tool.

TeaVM CLI created by TeaVM developer. It is one of main TeaVM tools. You can download the latest version of this tool via Maven Repository

Using this tool allows you to configure compilation directly without any Gradle or Maven plugins.

We will need to configure the compilation via command line in Gradle. To find out what command parameters can be used, run the tool without parameters, and it will show description of all parameters:

java -jar teavm.jar

To compile through TeaVM, we need to specify the following parameters:

  • classpath. Directories that must contain JVM class files .class, including classes from dependencies.
  • targetdir. Result directory for compiled files by TeaVM
  • At the end of all parameters, specify the class to that containing the main function. Class name must be fully qualified class name, for example com.example.Main.

Before we create a command execution task in Gradle, we need to create auxiliary tasks.

static Boolean checkDependency(File file) {
    def name = file.name
    if (name.contains("thelema")) return true
    return !(name.contains("teavm") ||
            name.contains("gson") ||
            name.contains("jzlib") ||
            name.contains("joda-time") ||
            name.contains("annotations")
    )
}

task copyRuntimeClasses(type: Copy) {
    dependsOn(build)

    configurations.runtimeClasspath.collect {
        if (checkDependency(it)) {
            from(zipTree(it).matching {
                include("**/*.class")
            })
        }
        true
    }

    includeEmptyDirs = false

    into("$buildDir/classes/dependencies")
}

task copyRuntimeResources(type: Copy) {
    from("$buildDir/resources/main")
    into("$buildDir/teavm")
}

We have created two tasks copyRuntimeClasses and copyRuntimeResources.

copyRuntimeClasses will unpack all jar dependencies and copy from them classes to the directory build/classes/dependencies. Later, we will pass this directory path to parameters for TeaVM.

We only take files that with .class extension. Also, using the checkDependency function, we exclude all unnecessary dependencies, in particular, these are the teavm dependencies.

With copyRuntimeResources we copy the resource files from src/main/resources to build/teavm.

Now we can create a task to compile through TeaVM.

task teavmCompile(type: Exec) {
    dependsOn(copyRuntimeClasses, copyRuntimeResources)

    commandLine([
            "java",
            "-jar",
            "teavm.jar",
            "--classpath",
            "$buildDir/classes/dependencies",
            "$buildDir/classes/kotlin/main",
            "--targetdir",
            "build/teavm",
            "--debug",
            "--sourcemaps",
            "com.example.Main"
    ])
}

type: Exec indicates that in this task Gradle should invoke the command via commandLine.

In classpath parameter, we pass the directory with dependency classes build/classes/dependencies and directory with generated classes of our application build/classes/kotlin/main.

In this example, we will enable debug options debug and sourcemaps.

If you want to see what dependencies will be added to compilation, you can write this code in the teavmCompile task.

println("=== dependencies ===")
configurations.runtimeClasspath.collect {
    if (checkDependency(it)) println(it.name)
    true
}

Now we can run teavmCompile task via IntelliJ in Gradle panel on the right in the Tasks -> Other.

As a result, TeaVM should generate several files including classes.js. These files should be located in build/teavm. index.html will be placed in the same directory, which we can open in the browser to see the result.

See build.gradle

screenshot