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.
Context
Phosphor has no voxel primitive today. The only 3D primitive in
phosphor-core/render/isCognitiveWaveform(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
VoxelSphereas a new construction primitive inphosphor-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
VoxelSphereandVoxelas a new field-class type inphosphor-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:(x, y, z)triples in[-resolution, resolution]and admits voxels wheresqrt(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)wheretargetWorldSize = 11f— calibrated to keep the rendered orb visually consistent size across resolution settings, matching the prototype's calibration.jitterper voxel is deterministic — derived from a stable hash of(gridX, gridY, gridZ), not fromRandom(). Each component is in roughly[-0.5, 0.5]. Reconstructing aVoxelSpherewith the same resolution produces voxels with identical jitter, which matters for snapshot serialization and visual regression testing.fun Voxel.facingCamera(orbQuaternion: Vector3, threshold: Float = 0.15f): Booleanreturning whether the voxel's unit direction (rotated by the orb's current quaternion) faces the viewer. Used by Wave 1's glyph carving logic. IfVector3rotation by a quaternion isn't already present inmath/, this ticket adds the supporting function inmath/Vector3.kt.commonTest/field/VoxelSphereTest.kt:VoxelSphere(7).countproduces the expected documented voxel count (state the exact number in the test).VoxelSphere(7).voxels.first().normalizedPoscomponents are all in[-1, 1].VoxelSphere(7).rebuild(7)is equivalent toVoxelSphere(7)(identity rebuild).worldScaleproduces the documented value at known resolutions.Technical constraints
Vector3andVector2types frommath/. If quaternion-style rotation isn't available, add it tomath/rather than inventing locally infield/.(gridX, gridY, gridZ)mapped to[-0.5, 0.5]per axis. No use ofkotlin.random.Randominside lattice construction.field/VoxelSphere.kt, sibling tofield/ParticleSystem.ktandfield/FlowLayer.kt.VoxelSphereconstruction; subsequent reads are O(1).Voxelis adata classso it composes with the rest of Phosphor's value-type conventions and serialization story.@Serializableannotation is not required in this ticket — add only iffield/types already carry it; otherwise defer to Wave 1 when serialization needs become concrete../gradlew jvmTestand./gradlew ktlintFormatbefore completion (perAGENTS.md). iOS compile gate is CI-enforced — verify the change builds on iOS.Out of scope
CognitiveSceneRuntimeorSceneSnapshot.