Three.js port for the JVM (desktop)

Overview

three.kt (Work in progress)

License: MIT contributions welcome

CI Gitter

Kotlin/JVM port of the popular three.js 3D library (r106).

Be warned, while the basics works, such as:

  • Primitives, Points and TubeGeometry
  • All materials and lights
  • OrbitControls
  • 2D textures
  • Raycasting against Mesh
  • OBJ, MTL and STL loaders
  • Other stuff like mirror, sky and water shaders

a lot of features are still missing and the API can change rapidly.

Right now, this is mostly interesting for developers that want to contribute.

API (subject to changes)

Kotlin
Window(antialias = 4).use { window ->

    val scene = Scene().apply {
        setBackground(Color.aliceblue)
    }

    val camera = PerspectiveCamera(75, window.aspect, 0.1, 1000).apply {
        position.z = 5f
    }

    val renderer = GLRenderer(window.size)

    val box = Mesh(BoxGeometry(1f), MeshBasicMaterial().apply {
        color.set(0x00ff00)
    })
    scene.add(box)
    
    val clock = Clock()
    val controls = OrbitControls(camera, window)
    window.animate {
     
        val dt = clock.getDelta()
        box.rotation.x += 1f * dt
        box.rotation.y += 1f * dt

        renderer.render(scene, camera)

    }

}
Java
public class JavaExample {

    public static void main(String[] args) {

        try (Window window = new Window()) {

            Scene scene = new Scene();
            PerspectiveCamera camera = new PerspectiveCamera();
            camera.getPosition().z = 5;
            GLRenderer renderer = new GLRenderer(window.getSize());

            BoxBufferGeometry boxBufferGeometry = new BoxBufferGeometry();
            MeshPhongMaterial boxMaterial = new MeshPhongMaterial();
            boxMaterial.getColor().set(Color.getRoyalblue());

            Mesh box = new Mesh(boxBufferGeometry, boxMaterial);
            scene.add(box);

            MeshBasicMaterial wireframeMaterial = new MeshBasicMaterial();
            wireframeMaterial.getColor().set(0x000000);
            wireframeMaterial.setWireframe(true);
            Mesh wireframe = new Mesh(box.getGeometry().clone(), wireframeMaterial);
            scene.add(wireframe);

            AmbientLight light = new AmbientLight();
            scene.add(light);

            OrbitControls orbitControls = new OrbitControls(camera, window);

            window.animate(() -> {
                renderer.render(scene, camera);
            });

        }

    }

}

Artifacts are available through Maven central:

repositories {
    mavenCentral()
}

dependencies {
    def version = "..."
    implementation "info.laht.threekt:core:$version"
}

Screenshots

seascape points ocean pointlights

Looking for the Kotlin/JS wrapper project?

It has been renamed and moved to here.

Comments
  • Move math classes to common.

    Move math classes to common.

    Moved all the math classes to common. Some of the functions had dependencies on jvm stuff or more complicated classes, so I made them extension functions for now. We can move them back to members once we move most of the codebase.

    Although, I'd argue we should leave them as extensions but java ease-of-use :man_shrugging: .

    opened by Dominaezzz 9
  • Migrate to multiplatform

    Migrate to multiplatform

    So I've setup a multiplatform project with only a JVM target for now. I expect we'll gradually move components to the common source set .... until we can't. Then we'll look at viable abstractions.

    There's one tiny gotcha. The play button IntelliJ offers when looking at a sample file won't work anymore because of a bug in IntelliJ (about resource handling) we discussed. So I've created a gradle task for each sample to run them.

    opened by Dominaezzz 8
  • Feature/add instanced mesh

    Feature/add instanced mesh

    With InstancedMesh we can render the same geometry thousands of times with only 1 draw call. This is a big performance boost :) I've implemented this example: https://threejs.org/examples/webgl_instancing_raycast

    CVyMBz8WsF

    Since r106 didn't have this feature, I ripped it from the last threejs release (r125).

    opened by Displee 7
  • Custom mipmaps not working

    Custom mipmaps not working

    I try to use my own generated mipmaps, but for whatever reason my geometry always turns black when I do this. I copied the textures example and only edited the material of the plane buffer geometry:

    package info.laht.threekt.examples.textures
    
    import info.laht.threekt.*
    import info.laht.threekt.cameras.PerspectiveCamera
    import info.laht.threekt.controls.OrbitControls
    import info.laht.threekt.geometries.BoxBufferGeometry
    import info.laht.threekt.geometries.PlaneBufferGeometry
    import info.laht.threekt.geometries.SphereBufferGeometry
    import info.laht.threekt.loaders.TextureLoader
    import info.laht.threekt.materials.MeshBasicMaterial
    import info.laht.threekt.math.Color
    import info.laht.threekt.math.DEG2RAD
    import info.laht.threekt.objects.Mesh
    import info.laht.threekt.renderers.GLRenderer
    import info.laht.threekt.scenes.Scene
    import info.laht.threekt.textures.Image
    import info.laht.threekt.textures.Texture
    import kotlinx.io.core.IoBuffer
    import org.lwjgl.BufferUtils
    import java.awt.image.DataBufferByte
    import java.io.File
    import javax.imageio.ImageIO
    
    object TextureExample {
    
    	@JvmStatic
    	fun main(args: Array<String>) {
    
    		Window(antialias = 4).use { canvas ->
    
    			val scene = Scene().apply {
    				setBackground(Color.aliceblue)
    			}
    
    			val camera = PerspectiveCamera(75, canvas.aspect, 0.1, 1000)
    			camera.position.z = 10f
    
    			val renderer = GLRenderer(canvas.size).apply {
    				checkShaderErrors = true
    			}
    
    			Mesh(PlaneBufferGeometry(10f, 10f), MeshBasicMaterial().apply {
    				color.set(Color.gray)
    				val repeatS = true
    				val repeatT = true
    				val useMipmaps = true
    				map = Texture(
    					format = TextureFormat.RGBA,
    					type = TextureType.UnsignedByte,
    					mapping = TextureMapping.UV,
    					wrapS = if (repeatS) TextureWrapping.Repeat else TextureWrapping.ClampToEdge,
    					wrapT = if (repeatT) TextureWrapping.Repeat else TextureWrapping.ClampToEdge,
    					minFilter = if (useMipmaps) TextureFilter.LinearMipMapLinear else TextureFilter.Linear,
    					magFilter = TextureFilter.Linear
    				)
    				val images = arrayOf(
    					loadImage("C:\\Users\\Displee\\Documents\\Projects\\Github\\rs-model-editor-kt\\texture_228_0.png"),
    					loadImage("C:\\Users\\Displee\\Documents\\Projects\\Github\\rs-model-editor-kt\\texture_228_1.png"),
    					loadImage("C:\\Users\\Displee\\Documents\\Projects\\Github\\rs-model-editor-kt\\texture_228_2.png"),
    					loadImage("C:\\Users\\Displee\\Documents\\Projects\\Github\\rs-model-editor-kt\\texture_228_3.png"),
    					loadImage("C:\\Users\\Displee\\Documents\\Projects\\Github\\rs-model-editor-kt\\texture_228_4.png"),
    					loadImage("C:\\Users\\Displee\\Documents\\Projects\\Github\\rs-model-editor-kt\\texture_228_5.png"),
    					loadImage("C:\\Users\\Displee\\Documents\\Projects\\Github\\rs-model-editor-kt\\texture_228_6.png"),
    				)
    				map?.image = images[0]
    				map?.mipmaps?.addAll(images)
    				map?.needsUpdate = true
    			}).also {
    				it.rotation.x = DEG2RAD * -90
    				it.translateZ(-1f)
    				scene.add(it)
    			}
    
    
    			Mesh(BoxBufferGeometry(1f), MeshBasicMaterial().apply {
    				color.set(Color.gray)
    				map = TextureLoader.load(javaClass.classLoader.getResource("textures/crate.gif")!!.file)
    			}).also {
    				it.translateY(2f)
    				scene.add(it)
    			}
    
    			Mesh(SphereBufferGeometry(0.5f), MeshBasicMaterial().apply {
    				color.set(Color.gray)
    				map = TextureLoader.load(javaClass.classLoader.getResource("textures/checker.png")!!.file)
    			}).also {
    				it.translateY(4f)
    				scene.add(it)
    			}
    
    			OrbitControls(camera, canvas)
    
    			canvas.animate {
    				renderer.render(scene, camera)
    			}
    
    		}
    
    	}
    
    	fun loadImage(path: String): Image {
    		val bufferedImage = ImageIO.read(File(path))
    		val byteArray = (bufferedImage.raster.dataBuffer as DataBufferByte).data
    		val textureImageBuffer = IoBuffer(BufferUtils.createByteBuffer(byteArray.size))
    		textureImageBuffer.writeFully(byteArray, 0, byteArray.size)
    		return Image(bufferedImage.width, bufferedImage.height, textureImageBuffer)
    	}
    
    }
    

    Result: Mipmaps example

    My mipmaps: 64x64: Mipmap 64x64 32x32: Mipmap 32x32 16x16: Mipmap 16x16 8x8: Mipmap 8x8 4x4: Mipmap 4x4 2x2: Mipmap 2x2 1x1: Mipmap 1x1

    Am I doing something wrong?

    bug 
    opened by Displee 5
  • Feature/drift fx

    Feature/drift fx

    DriftFX allows you to render any OpenGL content directly into JavaFX nodes. Direct means that there is no transfer between GPU and main memory. The textures never leave the GPU.

    This example puts threekt to the test. It renders multiple surfaces (FX nodes) in multiple threads by multiple threekt renderers (also in new windows!). It works great so far.

    Mme4TeTTtW

    However, I found one small issue which is outside of this PR, but is linked to threekt. I'll post it below this PR.

    opened by Displee 4
  • Fix bytes per element for `IntBuffer`.

    Fix bytes per element for `IntBuffer`.

    This is another critical bug fix. I've been stuck on this for months. My indices (geometry.setIndex) were correct but my model was messed up and I couldn't for the love of God figure out why. I traced it down all the way to the BYTES_PER_ELEMENT of threejs: https://github.com/mrdoob/three.js/blob/dev/src/renderers/webgl/WebGLAttributes.js#L57

    else if ( array instanceof Int32Array ) {
    
    			type = gl.INT;
    
    		}
    ...
    return {
    			buffer: buffer,
    			type: type,
    			bytesPerElement: array.BYTES_PER_ELEMENT,
    			version: attribute.version
    		}
    

    Int32Array.BYTES_PER_ELEMENT = 4

    in threekt we use 3 (which is incorrect):

    val (type, bytesPerElement) = when (attribute) {
                is IntBufferAttribute -> {
                    GL15.glBufferData(bufferType, attribute.buffer, usage)
                    GL11.GL_UNSIGNED_INT to 3 // this was the evil-doer
                }
                is FloatBufferAttribute -> {
                    GL15.glBufferData(bufferType, attribute.buffer, usage)
                    GL11.GL_FLOAT to 4
                }
            }
    

    this also solved the issue I had

    opened by Displee 4
  • JavaFX black spot

    JavaFX black spot

    For some reason, the right side of my application always turns black. I don't know what I'm doing wrong.

    java_TgwyuZMgNt

    
    import com.sun.prism.es2.JFXGLContext
    import cuchaz.jfxgl.CalledByEventsThread
    import cuchaz.jfxgl.CalledByMainThread
    import cuchaz.jfxgl.JFXGL
    import cuchaz.jfxgl.JFXGLLauncher
    import cuchaz.jfxgl.controls.OpenGLPane
    import info.laht.threekt.Window
    import info.laht.threekt.cameras.PerspectiveCamera
    import info.laht.threekt.controls.OrbitControls
    import info.laht.threekt.core.Clock
    import info.laht.threekt.helpers.AxesHelper
    import info.laht.threekt.helpers.GridHelper
    import info.laht.threekt.math.Color
    import info.laht.threekt.renderers.GLRenderer
    import info.laht.threekt.scenes.Scene
    import javafx.application.Application
    import javafx.fxml.FXML
    import javafx.fxml.FXMLLoader
    import javafx.fxml.Initializable
    import javafx.scene.Parent
    import javafx.scene.layout.BorderPane
    import javafx.stage.Stage
    import java.io.IOException
    import java.net.URL
    import java.util.*
    
    fun main(args: Array<String>) {
        JFXGLLauncher.launchMain(ResizableFX.javaClass, args)
    }
    
    object ResizableFX : Application() {
    
        private const val WIDTH = 1200
        private const val HEIGHT = 800
        private const val AAS = 4
        private val backgroundColor = Color(Color.darkgreen)
    
        private lateinit var PROPERTIES: Properties
        private val controller = ResizeableFXController()
    
        //Rendering stuff
        private val scene = Scene().apply {
            setBackground(backgroundColor)
        }
    
        private val clock = Clock()
        private lateinit var renderer: GLRenderer
        private lateinit var camera: PerspectiveCamera
        private lateinit var controls: OrbitControls
    
        //temp
        private var resized = false
    
        @JvmStatic
        fun jfxglmain(args: Array<String>) {
            Window("Resizable FX", WIDTH, HEIGHT, AAS, true, true).use { canvas ->
                renderer = GLRenderer(canvas.size)
                camera = PerspectiveCamera(45, canvas.aspect, 1, 1E6).apply {
                    position.z = 10f
                }
                controls = OrbitControls(camera, canvas).apply {
                    zoomSpeed = -1.0F
                }
                GridHelper(10, 10, Color(Color.blue), Color(Color.white)).also {
                    scene.add(it)
                    camera.lookAt(it.position)
                }
                AxesHelper(2.5).also {
                    scene.add(it)
                }
    
                JFXGL.start(canvas.hwnd, args, ResizableFX)
                canvas.animate {
                    render()
                    renderer.render(scene, camera)
                    JFXGL.render()
                }
                JFXGL.terminate()
            }
        }
    
        private fun render() {
            if (resized) {
                val container = controller.renderContainer
                val width = container.width.toInt()
                val height = container.height.toInt()
                renderer.setSize(width, height)
                camera.aspect = width.toFloat() / height.toFloat()
                camera.updateProjectionMatrix()
                camera.updateWorldMatrix()
                resized = false
                println("$width $height")
            }
        }
    
        @CalledByEventsThread
        @Throws(IOException::class)
        override fun start(stage: Stage) {
            val loader = FXMLLoader()
            loader.classLoader = javaClass.classLoader
            loader.setController(controller)
            val root = loader.load<Parent>(javaClass.getResourceAsStream("/example.fxml"))
            stage.scene = javafx.scene.Scene(root, WIDTH.toDouble(), HEIGHT.toDouble())
            stage.widthProperty().addListener { _, _, _ ->
                resized = true
            }
            stage.heightProperty().addListener { _, _, _ ->
                resized = true
            }
        }
    
        @CalledByMainThread
        fun render(context: JFXGLContext) {
    
        }
    
    }
    
    class ResizeableFXController : Initializable {
    
        @FXML
        lateinit var renderContainer: BorderPane
    
        override fun initialize(location: URL?, resources: ResourceBundle?) {
            val pane = OpenGLPane().apply {
                setRenderer { context -> ResizableFX.render(context) }
            }
            renderContainer.center = pane
        }
    
    }
    

    FXML:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Menu?>
    <?import javafx.scene.control.MenuBar?>
    <?import javafx.scene.control.MenuItem?>
    <?import javafx.scene.control.Separator?>
    <?import javafx.scene.layout.BorderPane?>
    
    
    <BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1">
       <top>
          <MenuBar BorderPane.alignment="CENTER">
            <menus>
              <Menu mnemonicParsing="false" text="File">
                <items>
                  <MenuItem mnemonicParsing="false" text="Close" />
                </items>
              </Menu>
              <Menu mnemonicParsing="false" text="Edit">
                <items>
                  <MenuItem mnemonicParsing="false" text="Delete" />
                </items>
              </Menu>
              <Menu mnemonicParsing="false" text="Help">
                <items>
                  <MenuItem mnemonicParsing="false" text="About" />
                </items>
              </Menu>
            </menus>
          </MenuBar>
       </top>
       <center>
          <BorderPane fx:id="renderContainer" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" />
       </center>
       <left>
          <BorderPane maxHeight="1.7976931348623157E308" maxWidth="-Infinity" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
             <right>
                <Separator orientation="VERTICAL" prefHeight="200.0" BorderPane.alignment="CENTER" />
             </right>
          </BorderPane>
       </left>
       <right>
          <BorderPane maxHeight="1.7976931348623157E308" maxWidth="-Infinity" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
             <left>
                <Separator orientation="VERTICAL" prefHeight="200.0" BorderPane.alignment="CENTER" />
             </left>
          </BorderPane>
       </right>
    </BorderPane>
    
    opened by Displee 4
  • Revert back to JVM only project

    Revert back to JVM only project

    I need to be selfish about this one. Currently this project has been setup as a Kotlin multiplatform project with JVM being the only implementation. The idea was that JS and Native would follow. I thought that sounded like a good idea and the initial JVM only project was converted into a multiplatform project.

    I fell this change has not been beneficial for me since I'm only interested in the JVM target and the change made it harder to develop that. Therefore I will revert back to JVM only. If someone wants multi-platform, they'll need to fork this project.

    Sorry, @Dominaezzz and @altavir, perhaps you can team up for a continued multiplatform project.

    opened by markaren 4
  • Fix mipmaps for 90%

    Fix mipmaps for 90%

    I wasted a week on fixing mipmaps. You're not going to believe the fix 😡

    However, there seems to be an issue with MeshLambertMaterial. When using that in the mipmaps example the mesh becomes black, but when using MeshBasicMaterial the textures work. Somehow MeshLambertMaterial is being treated differently.

    I tried to update the MeshLambertMaterial class and set the default values like threejs does in r106, but I had no luck yet.

    Forgive me for my useless "merge pull request" commits xD.

    opened by Displee 3
  • PSA: Only BufferGeometries are supported

    PSA: Only BufferGeometries are supported

    <\*>Geometry in three.kt are actually typeAliases for the <\*>BufferGeometry equivalent.

    I'm not planning to add the Geometry class. But give me a good reason why it should be included, and I might reconsider.

    discussion wanted 
    opened by markaren 3
  • Support MacOS development out of the box

    Support MacOS development out of the box

    The LWJGL natives from the build.gradle target windows. It would be nice to have an easy way to build and test Three.kt from MacOS without requiring edits to the build.gradle.

    opened by arocnies 3
  • all textures dissapear when one mesh gets invsible

    all textures dissapear when one mesh gets invsible

    Currently there's a dirty bug where textured meshes turn black (their texture gets disposed somehow?) when the visibility of one textured mesh is set to false: IIuM5R3MZM

    This is most likely a threekt bug.

    opened by Displee 1
Owner
Lars Ivar Hatledal
Associate professor @ NTNU Aalesund, Norway
Lars Ivar Hatledal
Kotlin port of my Unity TileRPG game

TileRPG Kotlin port of my Unity TileRPG game A libGDX project generated with gdx-liftoff. This project was generated with a Kotlin project template th

null 1 Jan 23, 2022
Cross-platform framework for building truly native mobile apps with Java or Kotlin. Write Once Run Anywhere support for iOS, Android, Desktop & Web.

Codename One - Cross Platform Native Apps with Java or Kotlin Codename One is a mobile first cross platform environment for Java and Kotlin developers

Codename One 1.4k Jan 9, 2023
🔖 A Quotes Application built to Demonstrate the Compose for Desktop UI

A Quotes Application built to Demonstrate the use of Jetpack Compose for building declarative UI in Desktop

Sanju S 60 Sep 9, 2022
Kotlin Multiplatform Sample - Android, iOS, Web, Desktop

KMP-Sample Kotlin Multiplatform Sample Android iOS Web (Compose for web) Desktop (Compose for desktop) ?? Structure Diagram ?? Build At least android

안홍범 14 Dec 13, 2022
A Kotlin Multiplatform Project using TMDB Api. Currently supports Android,iOS,Desktop and web platforms

A Kotlin Multiplatform Project using TMDB Api(https://www.themoviedb.org/). Currently this project is implemented in following platforms Andr

Jackson E J 11 Nov 10, 2022
A Kotlin Multiplatform and Compose template that allows you to easily set up your project targeting: Android, Desktop, and Web

A Kotlin Multiplatform and Compose template that allows you to easily set up your project targeting: Android, Desktop, and Web

Carlos Mota 3 Oct 27, 2021
Unsplash application for Android, Desktop and Web. Built using Kotlin Multiplatform and Compose

Unsplash Unsplash application for Android, Desktop and Web. Built using Kotlin Multiplatform and Compose with ❤️ ?? Presentation Set up the environmen

Carlos Mota 15 Nov 11, 2022
Location-history-viewer - Small compose-desktop app to view data from google's location history

Google Location History Takeout Viewer This application provides a minimalistic

Chris Stelzmüller 3 Jun 23, 2022
Funstuff - Minimal Kotlin Multiplatform project with SwiftUI, Jetpack Compose, Compose for Wear OS, Compose for Desktop

PeopleInSpace Minimal Kotlin Multiplatform project with SwiftUI, Jetpack Compose

Shivam Dhuria 2 Feb 15, 2022
🔴 A non-deterministic finite-state machine for Android & JVM that won't let you down

HAL is a non-deterministic finite-state machine for Android & JVM built with Coroutines StateFlow and LiveData. Why non-deterministic? Because in a no

Adriel Café 73 Nov 28, 2022
:blowfish: An Android & JVM key-value storage powered by Protobuf and Coroutines

PufferDB PufferDB is a ⚡ key-value storage powered by Protocol Buffers (aka Protobuf) and Coroutines. The purpose of this library is to provide an eff

Adriel Café 94 Dec 7, 2022
🚟 Lightweight, and simple scheduling library made for Kotlin (JVM)

Haru ?? Lightweight, and simple scheduling library made for Kotlin (JVM) Why did you build this? I built this library as a personal usage library to h

Noel 13 Dec 16, 2022
A injection minecraft cheat using jvm attach api

Luminous A injection minecraft cheat using jvm attach api Website: https://lumi.getfdp.today Build We used a thing called Wrapper to make development

null 24 Dec 21, 2022
Yet Another Native Loader for the JVM.

yanl - yet another native loader Yet another Native library extractor/loader for the JVM, written in Kotlin. why other libraries simply don't fit my n

Stardust Enterprises 5 Dec 23, 2022
A Template for a Github Actions Pipeline for building and publishing Gradle-JVM Applications

github-actions-cd-template-jvm A Template for a Github Actions Pipeline for building and publishing Gradle-JVM Applications It build a executable shad

Raphael Panic 0 Dec 5, 2021
Run Kotlin/JS libraries in Kotlin/JVM and Kotlin/Native programs

Zipline This library streamlines using Kotlin/JS libraries from Kotlin/JVM and Kotlin/Native programs. It makes it possible to do continuous deploymen

Cash App 1.5k Dec 30, 2022
Yaspeller-kt - Asynchronous Yandex.Speller API wrapper for Kotlin/JVM.

yaspeller-kt Asynchronous Yandex.Speller API wrapper for Kotlin/JVM. Installation repositories { maven { url 'https://jitpack.io' }

Mikhail Koshkin 6 Jun 27, 2022
Blog implemented via the Storyblok Kotlin Multiplatform SDK (Android, JVM)

storyblok-mp-SDK-blog ... a showcase of using the Storyblok Kotlin Multiplatform Client to build a blog application (Android, JVM) What's included ??

Mike Penz 5 Sep 28, 2022
Solves the audit needs for any JVM based application

Auditor-v1 Solves the audit needs for any JVM based application.

Lowe's 12 Dec 15, 2022