A multiplatform 3D game and rendering engine written in Kotlin, targeting desktop (JVM) and mobile (Android) using modern OpenGL 3.3/4.3 and OpenGL ES 3.0/3.2.
juniojsv-engine demonstrates how to build a unified, high-performance 3D engine in Kotlin Multiplatform (KMP) across desktop and mobile platforms without sacrificing platform-specific capabilities.
Instead of treating desktop and mobile as separate codebases, the engine shares the core mathematical model, scene hierarchy, entity lifecycle, physics simulation, mesh handling, and multi-pass rendering pipeline in common code (commonMain). Low-level primitives—such as graphic contexts, off-heap memory allocation, resource streaming, and UI overlays—are abstracted using Kotlin's expect/actual declarations.
graph TD
subgraph CoreEngine [":engine (Kotlin Multiplatform)"]
engineCommon["engine:commonMain<br/>• Scene & Entity Model<br/>• RenderPipeline & FBO Management<br/>• JBullet Physics Loop<br/>• GL Abstraction & Math<br/>• Shader Preprocessor"]
engineJvm["engine:jvmMain<br/>• LWJGL 3 (OpenGL 4.3)<br/>• GLFW Window & Events<br/>• ImGui Java Integration<br/>• Off-heap MemoryUtil"]
engineAndroid["engine:androidMain<br/>• Android GLES 3.2<br/>• GLSurfaceView.Renderer<br/>• ShaderConverter (GLSL to GLES)<br/>• Direct NIO Buffers"]
engineCommon -.-> engineJvm
engineCommon -.-> engineAndroid
end
subgraph ShowcaseModules ["Showcase Modules"]
exampleCommon[":example-common (KMP)<br/>• Shared MainScene<br/>• Asset Registry<br/>• Textures & Shaders"]
exampleJvm[":example-jvm (JVM Application)<br/>• Desktop Entrypoint (Main.kt)<br/>• GLFW Event Polling<br/>• ImGui Control Panel"]
exampleAndroid[":example-android (Android App)<br/>• MainActivity & GLSurfaceView<br/>• Jetpack Compose Virtual Gamepad"]
end
exampleCommon --> engineCommon
exampleJvm --> exampleCommon
exampleAndroid --> exampleCommon
| Module | Target | Responsibility |
|---|---|---|
:engine |
KMP (jvm, android) |
Core engine architecture: multi-pass render pipeline, multi-threaded physics, memory abstraction, instancing, bounding shapes, and platform GL bindings. |
:example-common |
KMP (jvm, android) |
Shared demonstration scene (MainScene), texture atlas loading, terrain generation, and dynamic mesh spawning. |
:example-jvm |
JVM (Application) | Desktop runtime: GLFW window management, keyboard/mouse first-person camera controller, and Dear ImGui debug interface. |
:example-android |
Android (Application) | Mobile runtime: Android Activity hosting GLSurfaceView with a Jetpack Compose touch gamepad overlay. |
Rendering is orchestrated by RenderPipeline through isolated framebuffers (FBOs):
sequenceDiagram
autonumber
participant Win as Window Loop
participant RP as RenderPipeline
participant ShadowFBO as Shadow FBO (8192x8192 Depth)
participant SceneFBO as Scene FBO (Color + Depth)
participant VelocityFBO as Velocity FBO (GL_RG16F)
participant OverlayFBO as Overlay FBO (Debug)
participant Composite as Fullscreen Viewport (window.frag)
Win->>RP: render(callbacks)
RP->>ShadowFBO: 1. Shadow Pass (Front-face culling, Orthographic Sun Matrix)
RP->>SceneFBO: 2. Scene Pass (Geometry, PCF Shadow Mapping, Dynamic Resolution Scale)
RP->>VelocityFBO: 3. Velocity Pass (Per-instance screen-space motion vectors)
RP->>OverlayFBO: 4. Overlay Pass (Depth blit from scene + wireframe debug draw)
RP->>Composite: 5. Post-Processing & Composite
Composite-->>Win: Final Frame with Depth-Aware Motion Blur & Mini Debug Viewports
-
Directional Shadow Mapping:
- Rendered to an
$8192 \times 8192$ depth-only buffer. - Front-face culling (
GL_FRONT) during the shadow pass significantly mitigates shadow acne and peter panning. - 8-tap Percentage Closer Filtering (PCF) with screen-space jittering and slope-scaled depth bias in
shadows.glsl.
- Rendered to an
-
Velocity Buffer & Per-Object Motion Blur:
- The velocity pass renders per-entity motion vectors into a half-float framebuffer (
GL_RG16F). - The composite shader
window.fragsamples along each pixel's velocity vector (up to 32 samples) with a depth-difference rejection threshold to prevent motion blur from bleeding across silhouette edges.
- The velocity pass renders per-entity motion vectors into a half-float framebuffer (
-
Dynamic Resolution Scaling:
- Framebuffers can be dynamically scaled via
resolutionScalewithout changing window or device screen resolution.
- Framebuffers can be dynamically scaled via
- Dedicated Thread Execution: The physics simulation (
PhysicsContext) runs in a background thread at a fixed 60 Hz tick (FIXED_TIME_STEP = 1f / 60f). - Thread Safety: World mutations (adding/removing rigid bodies, listeners, collision objects) are marshalled across threads using a lock-free queue (
ConcurrentLinkedQueue). - State Interpolation: Visual transforms are interpolated (
onSimulationInterpolate) between fixed simulation ticks to eliminate micro-stuttering during variable display refresh rates. - First-Person Character Controller:
PlayerControllerimplements grounded checks using continuous convex sweep tests (convexSweepTest), ground friction, jump impulses, and air control attenuation.
Rather than maintaining separate representations for rendering, physics, and debugging, shapes implement IBoundaryShape (BoundaryBox, BoundaryEllipsoid):
- Frustum Culling: Evaluates view-frustum plane equations (AABB, OBB with arbitrary orientation matrices, sphere, and ellipsoid) to drop off-screen draw calls.
- Physics Collision: Instantiates matching JBullet shapes (
BoxShape,ConvexHullShape). - Debug Visuals: Supplies transformation data to
DebugShapeRenderfor rendering line-strip wireframes.
MultiEntityRender renders thousands of identical meshes in a single draw call via glDrawElementsInstanced:
- Uses per-instance VBO attributes with divisor
1(glVertexAttribDivisor):- Current Model Matrix (
mat4) - Previous Model Matrix (
mat4) — required for instantaneous velocity buffer computation - Texture Index (
int) - UV Scale and UV Offset (
vec2)
- Current Model Matrix (
Shaders are authored once in desktop GLSL 330 with modular #include directives:
Shader.kthandles recursive#includeresolution at load time.- On Android,
ShaderConverter.ktautomatically translates the shader to OpenGL ES 3.0:- Rewrites
#version 330to#version 300 es. - Removes explicit
layout(location = ...)input/output declarations unsupported in ES 3.0 fragment shaders. - Injects
precision highp float;. - Normalizes floating-point numeric literals.
- Rewrites
Rather than relying on manually aligned sun positions, SkyBox and FileCubeMapTexture sample the pixels across all six cubemap faces to find the pixel of maximum luminance (faceWithMaxPixelLuminance). The engine automatically sets the primary ambient/directional light source's vector and color based on the brightest point in the skybox.
| Domain | Technologies |
|---|---|
| Language & Tooling | Kotlin 2.1.20 (Multiplatform), Gradle 8.12, Java 17 |
| Desktop Graphics & Windowing | LWJGL 3.2.3 (OpenGL 4.3, GLFW) |
| Mobile Graphics & UI | Android OpenGL ES 3.2 (GLSurfaceView), Jetpack Compose (BOM 2025.02.00) |
| Physics Simulation | JBullet (Java port of Bullet Physics) |
| Math & Matrix Operations | JOML 1.9.20 (Java OpenGL Math Library) |
| Mesh Loading | de.javagl:obj 0.3.0 (Wavefront .obj parser) |
| Desktop GUI / Debugging | Dear ImGui via imgui-java 1.81.0 (LWJGL3 / GLFW binding) |
| Logging | SLF4J 2.0.9 + Logback Classic (JVM) / Logback Android (Mobile) |
- Java Development Kit (JDK): Version 17 or higher.
- Desktop Target: Windows, Linux, or macOS x64/arm64 (LWJGL and ImGui natives are detected automatically from the host OS, or configurable via
-PdesktopNatives). - Android Target: Android SDK with API Level 34 (
compileSdk = 34,minSdk = 24) andANDROID_HOMEconfigured.
To launch the desktop demonstration with GLFW and ImGui (automatically downloads the correct native binaries for your operating system):
# Windows
.\gradlew.bat :example-jvm:run
# Linux / macOS
./gradlew :example-jvm:run
# Optionally specify natives or bundle all (windows, linux, macos, or all):
./gradlew :example-jvm:run -PdesktopNatives=allTo assemble the debug APK:
# Windows
.\gradlew.bat :example-android:assembleDebug
# Linux / macOS
./gradlew :example-android:assembleDebugThe APK will be generated at example-android/build/outputs/apk/debug/example-android-debug.apk.
To verify compilation across common and platform targets:
.\gradlew.bat :engine:assemble| Input | Action |
|---|---|
| Left Click | Toggle camera capture mode (locks/unlocks mouse cursor) |
| Mouse Look | Rotate active camera (Yaw / Pitch) |
| W / A / S / D | Move Forward / Left / Backward / Right |
| Space | Jump (Player mode) / Move Up (Free-look mode) |
| Left Shift | Move Down (Free-look mode) |
| C | Cycle between player camera and free-look camera |
| Esc | Close application |
| ImGui Panel | Configure active skybox, entity spawn count, instancing mode, physics speed, shadows toggle, and debug viewports in real time |
| Input | Action |
|---|---|
| Virtual Joystick (Left) | Directional movement (forward, backward, strafe) |
| Touch Drag (Right) | Camera look / rotation |
| Button A | Switch camera perspective |
| Button B | Jump |
| Button X | Cycle to next skybox |
| Button Y | Spawn 100 random physics entities |
- Platform Natives: Dynamic host OS native resolution is supported for Windows, Linux, and macOS. When building distributions or running on non-standard architectures, use
-PdesktopNatives=<platform|all>to explicitly choose or bundle the target native binaries. - Automated Tests: The project does not currently contain an automated unit test suite (
src/commonTest); functional and performance validation is handled via the sample applications. - Kotlin Compiler Flags: Uses internal compiler flags (
-Xexpect-actual-classesand language featureBreakContinueInInlineLambdas) configured in the root build scripts. - Library Distribution: Artifact publishing is targeted to GitHub Packages (
maven.pkg.github.com/JunioJsv/juniojsv-engine) via.github/workflows/publish.yaml.
This project is licensed under the MIT License. See LICENSE for details.
.gif?raw=true)