Skip to content

Voxel sphere construction primitive #21

@wow-miley

Description

@wow-miley

Context

Phosphor has no voxel primitive today. The only 3D primitive in phosphor-core/render/ is CognitiveWaveform (a deformable heightmap). The upcoming Lumos visualization requires a sphere-shaped voxel lattice — a 3D grid of cubes within a sphere radius, with per-voxel attributes captured once at construction time so renderers can iterate the lattice cheaply per frame.

The lattice and its per-voxel attributes are foundational geometry, useful beyond the immediate Lumos work for any future visualization that wants volumetric voxel content. This ticket adds VoxelSphere as a new construction primitive in phosphor-core/field/field/ houses Phosphor's other particulate/distributed primitives (ParticleSystem, FlowLayer, SubstrateState), and a sphere-of-voxels fits that taxonomy naturally.

This ticket adds only the geometry. Rendering, animation, and atmosphere integration are separate tickets in Wave 1.

Objective

Add VoxelSphere and Voxel as a new field-class type in phosphor-core/field/. Provide deterministic construction parameterized by resolution, with per-voxel attributes (position, normalized position, unit direction, spherical coordinates, jitter seed) computed once at build time and exposed as an immutable list.

Expected outcomes

A new field/VoxelSphere.kt:

data class Voxel(
    val gridX: Int,
    val gridY: Int,
    val gridZ: Int,
    val normalizedPos: Vector3,    // position / max(resolution, 1), range [-1, 1]
    val unitDirection: Vector3,    // normalized position direction, length 1
    val theta: Float,              // azimuthal angle from atan2(z, x)
    val phi: Float,                // polar angle from acos(y / dist)
    val distance: Float,           // distance from lattice origin
    val jitter: Vector3,           // deterministic per-voxel perturbation seed
)

class VoxelSphere(val resolution: Int) {
    val voxels: List<Voxel>
    val count: Int get() = voxels.size
    val worldScale: Float  // per-resolution scale that keeps visual orb size constant
  
    fun rebuild(newResolution: Int): VoxelSphere
  
    companion object {
        fun totalCount(resolution: Int): Int
    }
}
  • Lattice construction enumerates (x, y, z) triples in [-resolution, resolution] and admits voxels where sqrt(x² + y² + z²) ≤ resolution + 0.45 (the 0.45 inflation matches the Lumos prototype, producing a slightly fuller silhouette than a strict sphere).
  • worldScale = targetWorldSize / max(resolution, 1) where targetWorldSize = 11f — calibrated to keep the rendered orb visually consistent size across resolution settings, matching the prototype's calibration.
  • jitter per voxel is deterministic — derived from a stable hash of (gridX, gridY, gridZ), not from Random(). Each component is in roughly [-0.5, 0.5]. Reconstructing a VoxelSphere with the same resolution produces voxels with identical jitter, which matters for snapshot serialization and visual regression testing.
  • Utility extension fun Voxel.facingCamera(orbQuaternion: Vector3, threshold: Float = 0.15f): Boolean returning whether the voxel's unit direction (rotated by the orb's current quaternion) faces the viewer. Used by Wave 1's glyph carving logic. If Vector3 rotation by a quaternion isn't already present in math/, this ticket adds the supporting function in math/Vector3.kt.
  • Unit tests in commonTest/field/VoxelSphereTest.kt:
    • VoxelSphere(7).count produces the expected documented voxel count (state the exact number in the test).
    • VoxelSphere(7).voxels.first().normalizedPos components are all in [-1, 1].
    • Two independent constructions with the same resolution produce voxel-by-voxel identical results, including jitter.
    • VoxelSphere(7).rebuild(7) is equivalent to VoxelSphere(7) (identity rebuild).
    • worldScale produces the documented value at known resolutions.

Technical constraints

  • KMP common code, no platform-specific math.
  • Use existing Vector3 and Vector2 types from math/. If quaternion-style rotation isn't available, add it to math/ rather than inventing locally in field/.
  • Deterministic jitter: stable hash of (gridX, gridY, gridZ) mapped to [-0.5, 0.5] per axis. No use of kotlin.random.Random inside lattice construction.
  • File location: field/VoxelSphere.kt, sibling to field/ParticleSystem.kt and field/FlowLayer.kt.
  • The voxel list is constructed once during VoxelSphere construction; subsequent reads are O(1).
  • Voxel is a data class so it composes with the rest of Phosphor's value-type conventions and serialization story. @Serializable annotation is not required in this ticket — add only if field/ types already carry it; otherwise defer to Wave 1 when serialization needs become concrete.
  • Pure-core principle: zero UI dependencies.
  • Run ./gradlew jvmTest and ./gradlew ktlintFormat before completion (per AGENTS.md). iOS compile gate is CI-enforced — verify the change builds on iOS.

Out of scope

  • Any rendering of voxels.
  • Voxel animation per frame.
  • Glyph membership computation (2D shape predicates evaluated per frame — separate Wave 1 ticket).
  • Integration with CognitiveSceneRuntime or SceneSnapshot.
  • Pattern evaluation, color mapping, atmosphere consumption — all Wave 1.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions