Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
115ed79
Add LDraw files
colemancda Jul 7, 2026
4e1265c
Ignore LDraw library files
colemancda Jul 7, 2026
8b3e879
Add `Junkbot3D`
colemancda Jul 7, 2026
5c7a571
Add `LDrawModel`
colemancda Jul 7, 2026
ebcb5ef
Update `SceneBuilder`
colemancda Jul 7, 2026
81dd497
Refactor LDraw support and model loading
colemancda Jul 7, 2026
3841a9e
Add SceneKit renderer
colemancda Jul 7, 2026
78d2530
Update Junkbot model
colemancda Jul 7, 2026
708d3ad
Fix SceneKit projection
colemancda Jul 7, 2026
573a47b
Update Junkbot model
colemancda Jul 7, 2026
9a58ff3
Enhance possibleGrabsAt to sort grabbable entities by vertical position
colemancda Jul 7, 2026
64761f3
Improve SceneKit rendering
colemancda Jul 7, 2026
39e0480
Update `Palette`
colemancda Jul 7, 2026
5c6cd1a
Update `Scene3DManager`
colemancda Jul 7, 2026
7b3ab29
Enhance Junkbot3D animation and rendering features
colemancda Jul 7, 2026
d0a0e4d
Enhance physically-based materials and lighting in Scene3DManager and…
colemancda Jul 7, 2026
d1c2a36
Update dependencies
colemancda Jul 7, 2026
1e39bf5
Render 3D play mode on macOS via Metal instead of SceneKit
colemancda Jul 7, 2026
a26494c
Moved files
colemancda Jul 7, 2026
8672115
Add `Metal3DDecalTextures`
colemancda Jul 7, 2026
cd5b1ab
Port Metal 3D lighting from the three.js reference's actual scene setup
colemancda Jul 7, 2026
03aa0f5
Render level background decals (doors, arrows, windows) in the Metal …
colemancda Jul 7, 2026
abff378
Add a Vulkan 3D renderer for Android play mode
colemancda Jul 8, 2026
0ae7470
Render the NDS world in 3D via libnds' NitroGL instead of 2D sprites
colemancda Jul 8, 2026
f0de310
Add hardware directional lighting to the NDS 3D renderer
colemancda Jul 8, 2026
6ca235a
Add standalone viewer for low-poly 2x1 LEGO brick model
colemancda Jul 8, 2026
751eafe
Refactor NDS brick model implementation to use Mesh structure
colemancda Jul 8, 2026
268966a
Add low-poly models for Junkbot entities and update 3D rendering
colemancda Jul 8, 2026
13b6e7f
Add ModelStudio: a SwiftUI + SceneKit playground for authoring the lo…
colemancda Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Official LDraw parts library (~600MB extracted) - downloaded via
# https://library.ldraw.org/library/updates/complete.zip, not vendored in the repo.
tools/Junkbot3D/LDraw/

# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
Expand Down
1 change: 1 addition & 0 deletions Models3D
11 changes: 10 additions & 1 deletion ports/Android/AndroidApp/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ plugins {
// app/ -> AndroidApp/ -> Android/ -> ports/ -> repo root
def repoRoot = rootDir.parentFile.parentFile.parentFile
def stagedAssetsDir = layout.buildDirectory.dir('stagedAssets').get().asFile
def gameAssetDirs = ['images', 'font', 'levels', 'audio']
// `Models3D` is the same baked-entity-model directory (`.json`, used by `Vulkan3DManager.swift`/
// `Metal3DModel.swift`; the `.scn` siblings are Darwin/SceneKit-only, harmless to also ship) the
// macOS Metal renderer already consumes - see `tools/Junkbot3D/Sources/Junkbot3D/main.swift`'s
// `--bake-all`, which also writes `Models3D/junkbot_decal.png` for both renderers' chest emblem.
def gameAssetDirs = ['images', 'font', 'levels', 'audio', 'Models3D']

android {
namespace 'org.junkbot.game'
Expand Down Expand Up @@ -45,6 +49,11 @@ tasks.register('stageGameAssets', Copy) {
gameAssetDirs.each { dir ->
from(new File(repoRoot, dir)) { into dir }
}
// Precompiled SPIR-V (`ports/Android/Shaders/*.spv`, committed output of `glslc` run against
// the `.vert`/`.frag` sources in that same directory - see `Vulkan3DManager.swift`'s doc
// comment) - lives under `ports/Android/`, not repo root like the other asset dirs above, so
// it needs its own `from`/`into` pair rather than fitting `gameAssetDirs`'s pattern.
from(new File(rootDir.parentFile, 'Shaders')) { into 'Shaders' }
}

tasks.register('writeAssetManifest') {
Expand Down
11 changes: 10 additions & 1 deletion ports/Android/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,17 @@ let package = Package(
// way to drive its installed executor's job queue without calling the raw C function
// ourselves (see AndroidMain.swift's poll loop).
.systemLibrary(name: "CAndroidLooper"),
// The NDK's own `vulkan/vulkan.h` (+ `vulkan_android.h` platform header) - no Swift/Vulkan
// wrapper package exists anywhere, so `Vulkan3DManager.swift` calls the raw C API directly.
// `libvulkan.so` ships on-device (API 24+, every Vulkan-capable device) - not bundled by us.
.systemLibrary(name: "CVulkan"),
// The exact same sources as ports/SDL3's JunkbotSDL3 executable (Sources/JunkbotSDL3 is a
// symlink into ../SDL3), minus main.swift's top-level code - library targets can't contain
// top-level code, and Android's entry point is `SDL_main` below instead.
.target(
name: "JunkbotGame",
dependencies: [
"CVulkan",
.product(name: "JunkbotCore", package: "junkbot-swift"),
.product(name: "SDL3Swift", package: "SDL"),
.product(name: "SDL3Image", package: "SDL"),
Expand All @@ -53,7 +58,11 @@ let package = Package(
.product(name: "AndroidLooper", package: "swift-android-native"),
],
path: "Sources/JunkbotSDL3",
exclude: ["main.swift"]
exclude: ["main.swift"],
// The `Vulkan3DManager*.swift` files (also under `Sources/JunkbotSDL3` - see that
// directory's own doc comments) call the raw Vulkan C API - `libvulkan.so` ships on-device
// (API 24+, every Vulkan-capable device), not bundled by us.
linkerSettings: [.linkedLibrary("vulkan")]
),
.target(
name: "JunkbotAndroid",
Expand Down
13 changes: 13 additions & 0 deletions ports/Android/Shaders/backdrop.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#version 450
// Counterpart of `Metal3DShaderSource.swift`'s `backdrop_fragment_main` - plain texture sample, no
// lighting (matches `Scene3DManager`'s `.constant`-lit, ambient-zeroed backdrop material: shows
// the image's exact pixel colors).

layout(location = 0) in vec2 inUV;
layout(location = 0) out vec4 outColor;

layout(binding = 1) uniform sampler2D tex;

void main() {
outColor = texture(tex, inUV);
}
Binary file added ports/Android/Shaders/backdrop.frag.spv
Binary file not shown.
19 changes: 19 additions & 0 deletions ports/Android/Shaders/backdrop.vert
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#version 450
// Textured-quad pipeline (backdrop image, background/foreground decals, Junkbot's chest emblem) -
// counterpart of `Metal3DShaderSource.swift`'s `backdrop_vertex_main`. Positions arrive already in
// world space (built on the CPU each draw, same as the Metal path's `drawTexturedQuad`), so
// `modelViewProjection` here is really just the camera's view-projection.

layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec2 inUV;

layout(location = 0) out vec2 outUV;

layout(binding = 0) uniform Uniforms {
mat4 modelViewProjection;
} uniforms;

void main() {
gl_Position = uniforms.modelViewProjection * vec4(inPosition, 1.0);
outUV = inUV;
}
Binary file added ports/Android/Shaders/backdrop.vert.spv
Binary file not shown.
26 changes: 26 additions & 0 deletions ports/Android/Shaders/vulkan3d.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#version 450
// Lighting ported from the same source `Metal3DShaderSource.swift`'s `fragment_main` documents:
// the original JS reference's own three.js scene setup (`three-stuff/3d-main.js`'s `setupScene`,
// ~line 107) - `THREE.AmbientLight(0xdedede, 0.8)` added flatly, `THREE.DirectionalLight(0xffffff,
// 0.8)` at `(-1000, 3200, 1500)` (aimed at the origin) scaled by `N.L`. Kept in sync with that
// file if the reference lighting ever changes - see its doc comment for the full rationale
// (matches `SceneBuilder.swift`'s SceneKit port of the same light; no per-part roughness/
// metalness term, since the baked vertex format carries no material data, same simplification
// `LDrawMSLSource.swift`/`Metal3DShaderSource.swift` both already made).

layout(location = 0) in vec3 inNormal;
layout(location = 1) in vec4 inColor;

layout(location = 0) out vec4 outColor;

void main() {
vec3 sunDirection = normalize(vec3(-1000.0, 3200.0, 1500.0));
vec3 ambientColor = vec3(0xde, 0xde, 0xde) / 255.0 * 0.8;
vec3 sunColor = vec3(1.0, 1.0, 1.0) * 0.8;

vec3 n = normalize(inNormal);
float ndotl = max(dot(n, sunDirection), 0.0);
vec3 light = ambientColor + sunColor * ndotl;

outColor = vec4(inColor.rgb * light, inColor.a);
}
Binary file added ports/Android/Shaders/vulkan3d.frag.spv
Binary file not shown.
22 changes: 22 additions & 0 deletions ports/Android/Shaders/vulkan3d.vert
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#version 450
// Main entity/brick pipeline vertex shader - counterpart of `Metal3DShaderSource.swift`'s
// `vertex_main`. Positions/normals arrive already baked into *world* space (see
// `Vulkan3DManager.swift`'s doc comment, same CPU-side approach `Metal3DManager.swift` uses), so
// `viewProjection` is the only transform left to apply here.

layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec4 inColor;

layout(location = 0) out vec3 outNormal;
layout(location = 1) out vec4 outColor;

layout(binding = 0) uniform Uniforms {
mat4 viewProjection;
} uniforms;

void main() {
gl_Position = uniforms.viewProjection * vec4(inPosition, 1.0);
outNormal = inNormal;
outColor = inColor;
}
Binary file added ports/Android/Shaders/vulkan3d.vert.spv
Binary file not shown.
4 changes: 4 additions & 0 deletions ports/Android/Sources/CVulkan/module.modulemap
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module CVulkan [system] {
header "shim.h"
export *
}
21 changes: 21 additions & 0 deletions ports/Android/Sources/CVulkan/shim.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Bridges the Android NDK's own Vulkan headers (ships `vulkan/vulkan.h` plus the
// `vulkan/vulkan_android.h` platform header under the NDK sysroot - no separate Vulkan SDK
// install needed) into a Swift-importable module, following the exact pattern
// `ports/Android/Sources/CAndroidLooper/shim.h` already uses for `<android/looper.h>`.
//
// `VK_USE_PLATFORM_ANDROID_KHR` must be defined *before* including `vulkan.h` to pull in
// `VkAndroidSurfaceCreateInfoKHR`/`vkCreateAndroidSurfaceKHR` (used to build a `VkSurfaceKHR`
// straight from the `ANativeWindow` SDL's own Vulkan glue exposes via `SDL_Vulkan_CreateSurface`
// - see `Vulkan3DManager.swift`'s doc comment for why raw `SDL_Vulkan_*` calls are used instead of
// hand-rolling `ANativeWindow` access ourselves).
#define VK_USE_PLATFORM_ANDROID_KHR
#include <vulkan/vulkan.h>

// `PureSwift/SDL`'s own `CSDL3` module only includes `<SDL3/SDL.h>` (the umbrella header), which
// does *not* pull in `SDL_vulkan.h` - `SDL_Vulkan_GetInstanceExtensions`/`SDL_Vulkan_CreateSurface`
// (used by `Vulkan3DManagerSetup.swift` to build the `VkInstance`/`VkSurfaceKHR`) are invisible
// through `import CSDL3` alone. Included here instead, after `vulkan.h` above, so its own
// `#ifdef VULKAN_CORE_H_` guard skips its duplicate `VkInstance`/`VkSurfaceKHR` typedefs and reuses
// the real ones from `vulkan.h` - one consistent set of Vulkan handle types, not two clashing ones
// from two separately-compiled Clang modules.
#include <SDL3/SDL_vulkan.h>
Loading
Loading