Skip to content

Render 3D play mode on macOS via Metal instead of SceneKit#19

Open
colemancda wants to merge 29 commits into
mainfrom
feature/3d
Open

Render 3D play mode on macOS via Metal instead of SceneKit#19
colemancda wants to merge 29 commits into
mainfrom
feature/3d

Conversation

@colemancda

Copy link
Copy Markdown
Owner

Summary

  • Adds a custom Metal renderer (Metal3DManager and supporting files) that replaces SceneKit as the live 3D play-mode rendering path on macOS: procedural brick geometry, all 15 baked LDraw entity models (including Junkbot's animated walk-cycle legs), and the level backdrop image, framed with the same oblique-shear + orthographic camera the SceneKit path used.
  • tools/Junkbot3D gains a parallel bake step (Metal3DExporter) that flattens each entity's LDraw model into JSON vertex data alongside the existing .scn bake output.
  • Scene3DManager and the rest of the SceneKit path stay in the tree, unused on macOS - iOS/tvOS are unaffected and still render 3D mode via SceneKit.

Test plan

  • tools/Junkbot3D builds and --bake-all regenerates Models3D/*.json alongside .scn
  • xcodebuild -scheme Junkbot-macOS builds successfully
  • Manually verified in the running macOS app: bricks render with correct colors, Junkbot's legs animate through the walk cycle, backdrop panel renders correctly

colemancda added 29 commits July 7, 2026 15:58
- Updated junkbot.ldr model with detailed comments and improved structure.
- Introduced Package.resolved to manage dependencies with swift-lego-draw.
- Modified Package.swift to include swift-lego-draw as a dependency.
- Removed obsolete LDrawColorTable and LDrawLoader implementations.
- Added LDrawSupport for parsing and resolving LDraw models using swift-lego-draw.
- Enhanced LDrawModel to utilize new LDrawSupport for loading models.
- Updated SceneBuilder to support oblique shear transformation for rendering.
- Improved main.swift to handle new command-line options and integrate LDrawSupport.
Adds a custom Metal renderer (Metal3DManager and friends) that replaces
SceneKit as the live 3D rendering path on macOS: procedural brick geometry,
all 15 baked LDraw entity models including Junkbot's animated walk-cycle
legs, and the level backdrop image, framed with the same oblique-shear +
orthographic camera the SceneKit path used. tools/Junkbot3D gains a parallel
bake step that flattens each entity's LDraw model into JSON vertex data
(Metal3DExporter) alongside the existing .scn bake output.

Scene3DManager and the rest of the SceneKit path stay in the tree, unused
on macOS - iOS/tvOS still render 3D mode via SceneKit as before.
Replaces the shader's ad hoc key/fill light weights (no reference behind
their values) with the exact ambient/directional light color and intensity
from three-stuff/3d-main.js's setupScene: AmbientLight(0xdedede, 0.8) added
flatly, DirectionalLight(0xffffff, 0.8) at (-1000, 3200, 1500) scaled by
N.L - matching the same light SceneBuilder.swift already ported to SceneKit.
…3D path

GameEngine.backgroundDecals/decals were internal, making them unreachable
from JunkbotMobile entirely - neither the SceneKit nor Metal 3D renderer
ever drew them, even though the 2D path always has. Makes both properties
public (matching backdropSpriteID's existing precedent) and adds
Metal3DManager.loadLevelDecals, which resolves each decal's sprite and
draws it as an unsheared textured quad at RenderList.swift's exact
per-layer offsets, layered between the backdrop and entities.
Mirrors the macOS Metal renderer's scope (procedural bricks, all 15 baked
LDraw entity models incl. Junkbot's walk-cycle rig, backdrop image, level
background decals) using raw Vulkan calls via a new CVulkan system-library
target bridging the NDK's own headers - no Swift/Vulkan wrapper package
exists anywhere. Reuses the portable Metal3DSpace/Palette/Matrix/Model/
BrickGeometry files unchanged across platforms via a small SIMDShim.swift
(Apple's simd module - float4x4 and friends - doesn't exist on Linux/
Android). Shaders are GLSL compiled to SPIR-V offline via glslc and
committed, following this repo's existing bake-and-commit pattern. Junkbot's
chest emblem is now baked to a PNG offline too (CoreGraphics isn't available
on Android), shared by both the Metal and Vulkan paths.

Also fixes Settings.swift never being symlinked into the shared SDL3/SDL2/
Android source tree despite Screens.swift (which all three build) already
referencing Settings.render3DEnabled - every non-Darwin port's build was
broken by this before now.
The Nintendo DS port now draws bricks and entities as flat-colored 3D boxes
on the hardware 3D layer (MODE_5_3D's BG0) instead of blitting 2D sprites
into a bitmap background. The baked LDraw geometry the Metal/Vulkan ports use
is far too large (~8-10MB vs a ~2.48MB ARM9 budget) and too dense (a single
model can exceed the DS GPU's ~3-4k triangle/frame ceiling), so every
brick/entity is a single procedural box (Geometry3D's unit cube, scaled and
positioned per draw) sized to its sprite footprint.

The 3D look comes from the same oblique-projection shear the web three.js
reference (three-stuff/3d-main.js's obliqueProjection) and the macOS Metal
renderer use - alpha=pi/4, Szx=-0.5cos, Szy=-0.5sin - injected into the DS
projection matrix via glMultMatrix4x4, with Szy sign-flipped for this port's
+Y-screen-down convention. The room backdrop stays a 2D bitmap on BG3,
composited behind the transparent-cleared 3D layer in hardware; the top
screen's text is untouched.
Each box's faces are now shaded by the DS's fixed-function lighting engine
instead of a single flat color, giving the flat-colored boxes readable depth
(lighter top, darker sides). Geometry3D emits a per-face normal; Renderer3D
sets up one directional light matching the three.js/Metal sun direction plus
a strong ambient floor (ambient+diffuse material split so a lit face reads at
full color and a shadowed face at ~half, never black).

The per-box non-uniform scale is applied in GL_POSITION matrix mode only, so
it never reaches the vector (normal) matrix - kept at identity - which would
otherwise distort the normals and wreck the shading.
- Introduced a new `Models` struct containing hand-authored low-poly models for various Junkbot entities, including bricks and characters.
- Implemented a `Mesh` structure to represent the geometry of these models.
- Updated the `Renderer3D` to utilize the new models, replacing the previous sprite-blit method with 3D rendering using the DS's fixed-function GPU.
- Added caching for entity and brick models to optimize rendering performance.
- Adjusted lighting and material settings to ensure consistent visual output across different entity types.
…w-poly models

A Swift App Playground (tools/ModelStudio.swiftpm) that previews the same
hand-authored low-poly entity models the Nintendo DS port renders, but with a
live turntable, a sidebar list, and a fast edit/build loop instead of
rebuilding a .nds and loading an emulator.

Each model is written in a small Swift '3D DSL' (MeshDSL.swift): a
@resultBuilder of value-type primitives - Box, Stud, CylinderY, CylinderX,
Disc, Cone - that compile straight to per-color SceneKit geometry, lit with an
ambient floor + directional key light chosen to match the DS renderer. One
file per model under Models/, registered in ModelLibrary. The DSL mirrors the
DS Mesh primitives and stud-unit conventions so a model dialed in here ports
directly to the fixed-point DS builder.

Materials are double-sided (the primitives don't emit a consistent winding),
mirroring the DS renderer's POLY_CULL_NONE.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant