tech spec 1.0, first working understanding of pos bytes encoding able to write world coords of all voxels

This commit is contained in:
Harvey Fong 2025-03-17 19:40:11 -06:00
parent 11279d162b
commit 7d37f4d994
3 changed files with 2034 additions and 511 deletions

324
README.md
View File

@ -62,58 +62,103 @@ root
└── snapshots (array)
└── Each snapshot (dictionary)
├── s (dictionary) - Snapshot data
│ ├── lc (binary data) - Location table
│ ├── id (dictionary) - Identifiers
│ │ ├── c (int64) - Chunk ID
│ │ ├── s (int64) - Session ID
│ │ └── t (int64) - Type ID
│ ├── lc (binary data) - Layer Color Usage
│ ├── ds (binary data) - Voxel data stream
│ ├── dlc (binary data) - Default layer colors
│ ├── dlc (binary data) - Deselected Layer Color Usage
│ └── st (dictionary) - Statistics/metadata
└── Other metadata fields:
├── cid (chunk id)
├── sid (edit session id)
└── t (type - VXVolumeSnapshotType)
│ ├── c (int64) - Count of voxels in the chunk
│ ├── sc (int64) - Selected Count (number of selected voxels)
│ ├── smin (array) - Selected Minimum coordinates [x,y,z,w]
│ ├── smax (array) - Selected Maximum coordinates [x,y,z,w]
│ ├── min (array) - Minimum coordinates of all voxels [x,y,z]
│ ├── max (array) - Maximum coordinates of all voxels [x,y,z]
│ └── e (dictionary) - Extent
│ ├── o (array) - Origin/reference point [x,y,z]
│ └── s (array) - Size/dimensions [width,height,depth]
```
## Chunking System
### Volume Organization
- The total volume is divided into chunks for efficient storage and manipulation
- Standard chunk size: 8×8×8 voxels
- Total addressable space: 256×256×256 voxels (32×32×32 chunks)
- Standard chunk size: 32×32×32 voxels
- Total addressable space: 256×256×256 voxels (8×8×8 chunks)
### Morton Encoding for Chunks
- Chunk IDs are encoded using 24 bits (8 bits per dimension)
- This allows addressing up to 256 chunks in each dimension , although only 8x8x8 are used in practice
- The decodeMortonChunkID function extracts x, y, z coordinates from a Morton-encoded chunk ID
- The resulting chunk coordinates are then multiplied by 32 to get the world position of the chunk
### Chunk Addressing
- Each chunk has 3D coordinates (chunk_x, chunk_y, chunk_z)
- ie a 16×16×16 volume with 8×8×8 chunks, there would be 2×2×2 chunks total
- For a 128×128×128 volume with 32×32×32 chunks, there would be 4×4×4 chunks total (64 chunks)
- Chunks are only stored if they contain at least one non-empty voxel
- Each snapshot contains data for a specific chunk, identified by the 'c' value in the 's.id' dictionary
## Data Fields
### Location Table (lc)
- Fixed-size binary array (256 bytes)
- Each byte represents a position in a 3D volume
- Non-zero values (typically 1) indicate occupied positions
- The index of each byte is interpreted as a Morton code for the position
- Size of 256 bytes limits addressable chunks to 256
- ie a 16×16×16 volume would get 16÷8 = 2 chunks, Total chunks = 2×2×2 = 8 chunks total
### Voxel Data Stream (ds)
- Variable-length binary data
- Contains pairs of bytes for each voxel: [position_byte, color_byte]
- Each chunk can contain up to 32,768 voxels (32×32×32)
- *Position Byte:*
- Usually 0 (meaning position at origin of chunk)
- Can encode a local position within a chunk using Morton encoding
- The format uses a encoding approach that combines sequential and Morton encoding for optimal storage efficiency:
- Uses a mix of position=0 bytes (for sequential implicit positions) and Morton-encoded position bytes
- The decoder maintains an internal position counter that advances through the chunk in a predefined order (x, then y, then z)
- Color byte 0 indicates "no voxel at this position" (empty space)
- If a chunk uses the entire 256x256x256 addressable space, then it uses exactly 65,536 bytes (32,768 voxel pairs)
- This is the dense case and it not memory efficient
- When we introduce morton encoding we can jump to a specific position in the chunk
- Data stream can terminate at any point, avoiding the need to store all 32,768 voxel pairs
### Morton Encoding Process
- A space-filling curve that interleaves the bits of the x, y, and z coordinates
- Used to convert 3D coordinates to a 1D index and vice versa
- Creates a coherent ordering of voxels that preserves spatial locality
1. Take the binary representation of x, y, and z coordinates
2. Interleave the bits in the order: z₀, y₀, x₀, z₁, y₁, x₁, z₂, y₂, x₂, ...
3. The resulting binary number is the Morton code
- *Color Byte:*
- Stores the color value + 1 (offset of +1 from actual color)
- Value 0 would represent -1 (typically not used)
- Value 0 indicates no voxel at this position
- A fully populated chunk will have 32,768 voxel pairs (65,536 bytes total in ds)
### Default Layer Colors (dlc)
- Optional 256-byte array
- May contain default color information for specific layers
### Snapshot Accumulation
- Each snapshot contains data for a specific chunk (identified by the chunk ID)
- Multiple snapshots together build up the complete voxel model
- Later snapshots for the same chunk ID overwrite earlier ones, allowing for edits over time
### Layer Color Usagw (lc)
- s.lc is a summary table (256 bytes) that tracks which colors are used anywhere in the chunk
- Each byte position (0-255) corresponds to a color palette ID
- [TODO] understand why the word layer color is used, what is a layer color
### Deselected Layer Color Usage (dlc)
- Optional 256-byte array
- Used during editing to track which color layers the user has deselected
- Primarily for UI state preservation rather than 3D model representation
### Statistics Data (st)
- Dictionary containing metadata like:
- Dictionary containing metadata about the voxels in a chunk:
- c (count): Total number of voxels in the chunk
- sc (selectedCount): Number of currently selected voxels
- sMin (selectedMin): Array defining minimum coordinates of current selection [x,y,z,w]
- sMax (selectedMax): Array defining maximum coordinates of current selection [x,y,z,w]
- min: Array defining minimum coordinates of all voxels [x,y,z]
- max: Array defining maximum coordinates of all voxels [x,y,z]
- e (extent): Array defining the bounding box [min_x, min_y, min_z, max_x, max_y, max_z]
- Other statistics fields may include count, max, min, etc.
- e.o (extent.origin): Reference point or offset for extent calculations
## Coordinate Systems
### Primary Coordinate System
- Y-up coordinate system: Y is the vertical axis
- Origin (0,0,0) is at the bottom-left-front corner
- Coordinates increase toward right (X+), up (Y+), and backward (Z+)
### Addressing Scheme
1. World Space: Absolute coordinates in the full volume
2. Chunk Space: Which chunk contains a voxel (chunk_x, chunk_y, chunk_z)
@ -121,32 +166,20 @@ root
## Coordinate Conversion
- *World to Chunk:*
- chunk_x = floor(world_x / 8)
- chunk_y = floor(world_y / 8)
- chunk_z = floor(world_z / 8)
- chunk_x = floor(world_x / 32)
- chunk_y = floor(world_y / 32)
- chunk_z = floor(world_z / 32)
- *World to Local:*
- local_x = world_x % 8
- local_y = world_y % 8
- local_z = world_z % 8
- local_x = world_x % 32
- local_y = world_y % 32
- local_z = world_z % 32
- *Chunk+Local to World:*
- world_x = chunk_x * 8 + local_x
- world_y = chunk_y * 8 + local_y
- world_z = chunk_z * 8 + local_z
## Morton Encoding
### Morton Code (Z-order curve)
- A space-filling curve that interleaves the bits of the x, y, and z coordinates
- Used to convert 3D coordinates to a 1D index and vice versa
- Creates a coherent ordering of voxels that preserves spatial locality
### Encoding Process
1. Take the binary representation of x, y, and z coordinates
2. Interleave the bits in the order: z₀, y₀, x₀, z₁, y₁, x₁, z₂, y₂, x₂, ...
3. The resulting binary number is the Morton code
### For Chunk Indexing
- Morton code is used to map the 3D chunk coordinates to a 1D index in the lc array
Formula: index = interleave_bits(chunk_x, chunk_y, chunk_z)
- world_x = chunk_x * 32 + local_x
- world_y = chunk_y * 32 + local_y
- world_z = chunk_z * 32 + local_z
### Detailed Example
### Detailed Morton Encoding Example
To clarify the bit interleaving process, here's a step-by-step example:
For position (3,1,2):
@ -165,185 +198,36 @@ For position (3,1,2):
Therefore, position (3,1,2) has Morton index 134.
An 8×8×8 chunk contains 512 voxels total, with Morton indices ranging from 0 to 511:
Morton index 0 corresponds to position (0,0,0)
Morton index 511 corresponds to position (7,7,7)
The Morton indices are organized by z-layer:
Z=0 layer: indices 0-63
Z=1 layer: indices 64-127
Z=2 layer: indices 128-191
Z=3 layer: indices 192-255
Z=4 layer: indices 256-319
Z=5 layer: indices 320-383
Z=6 layer: indices 384-447
Z=7 layer: indices 448-511
Each z-layer contains 64 positions (8×8), and with 8 layers, we get the full 512 positions for the chunk.
### For Local Positions
The position byte in the voxel data can contain a Morton-encoded local position
- Typically 0 (representing the origin of the chunk)
- When non-zero, it encodes a specific position within the chunk
## Color Representation
### Color Values
- Stored as a single byte (8-bit)
- Range: 0-255
- Important: Stored with an offset of +1 from actual color
### Color Interpretation
Format doesn't specify a specific color model (RGB, palette, etc.)
- Interpretation depends on the implementation
## Snapshots and Edit History
###Snapshots
- Each snapshot represents a single edit operation
- Multiple snapshots build up the complete voxel model over time
- Later snapshots override earlier ones for the same positions
### Snapshot Metadata
- cid (Chunk ID): Which chunk was modified
- sid (Session ID): Identifies the editing session
- t (Type): Type of snapshot (VXVolumeSnapshotType)
## Special Considerations
### Sparse Representation
- Only non-empty chunks and voxels are stored
- This creates an efficient representation for mostly empty volumes
### Position Encoding
- When all voxels in a chunk are at the origin (0,0,0), the position byte is always 0
- For more complex structures, the position byte encodes local positions within the chunk
### Color Offset
- The +1 offset for colors must be accounted for when reading and writing
- This might be to reserve 0 as a special value (e.g., for "no color")
### Field Sizes
- The location table (lc) is fixed at 256 bytes
- The voxel data stream (ds) varies based on the number of voxels
- For a simple 16×16×16 volume with 8×8×8 chunks, only 64 chunks can be addressed (2×2×2 chunks = 8 chunks total)
## Implementation Guidance
### Reading Algorithm
1. Parse the plist file to access the snapshot array
2. For each snapshot, extract the lc and ds data
3. Scan the lc data for non-zero entries to identify occupied positions
4. For each non-zero entry, decode the Morton index to get the chunk coordinates
5. Process the ds data in pairs of bytes (position, color)
6. For each voxel, calculate its absolute position and store with the corrected color (subtract 1)
7. Combine all snapshots to build the complete voxel model
2. For each snapshot:
a. Extract the chunk ID from s > id > c
b. Extract the lc and ds data
c. Process the ds data in pairs of bytes (position, color)
d. Calculate the world origin by decoding the Morton chunk ID and multiplying by 32
e. Store the voxels for this chunk ID
3. Combine all snapshots to build the complete voxel model, using the chunk IDs as keys
### Writing Algorithm
1. Organize voxels by chunk
1. Organize voxels by chunk (32×32×32 voxels per chunk)
2. For each non-empty chunk:
3. Create a snapshot entry
4. Set up a 256-byte lc array (all zeros)
5. Set the appropriate byte in lc to 1 based on the chunk's Morton code
6. Create the ds data by encoding each voxel as a (position, color+1) pair
7. Add metadata as needed
8. Add all snapshots to the array
9. Write the complete structure to a plist file
10. Compress with LZFSE
12. [hand wabving] Package up in a MacOS style package directory with some other files
## Practical Limits
- 256-byte location table can address up to 256 distinct positions
- 8-bit color values allow 256 different colors (including the offset)
- For a 256×256×256 volume with 8×8×8 chunks, there would be 32×32×32 = 32,768 chunks total
- The format could efficiently represent volumes with millions of voxels
a. Create a snapshot entry
b. Set up the id dictionary with the appropriate chunk ID
c. Set up a 256-byte lc array (all zeros)
d. Create the ds data by encoding each voxel as a (position, color+1) pair
e. Set the appropriate byte in lc to 1 if the color is used in ds
3. Add all snapshots to the array
4. Write the complete structure to a plist file
- [?] Models typically use SessionIDs to group related edits (observed values include 10 and 18)
-----
## Snapshot Types
The 't' field in the snapshot's 's.id' dictionary indicates the type of snapshot:
- 0: underRestore - Snapshot being restored from a previous state
- 1: redoRestore - Snapshot being restored during a redo operation
- 2: undo - Snapshot created for an undo operation
- 3: redo - Snapshot created for a redo operation
- 4: checkpoint - Snapshot created as a regular checkpoint during editing (most common)
- 5: selection - Snapshot representing a selection operation
# Scratch notes
```
Morton Binary Coords Visual (Z-layers)
Index (zyx bits) (x,y,z)
0 000 (000) (0,0,0) Z=0 layer: Z=1 layer:
1 001 (001) (1,0,0) +---+---+ +---+---+
2 010 (010) (0,1,0) | 0 | 1 | | 4 | 5 |
3 011 (011) (1,1,0) +---+---+ +---+---+
4 100 (100) (0,0,1) | 2 | 3 | | 6 | 7 |
5 101 (101) (1,0,1) +---+---+ +---+---+
6 110 (110) (0,1,1)
7 111 (111) (1,1,1)
```
In memory, this would be stored linearly:
```
Memory: [0][1][2][3][4][5][6][7]
| | | | | | | |
v v v v v v v v
Coords: (0,0,0)(1,0,0)(0,1,0)(1,1,0)(0,0,1)(1,0,1)(0,1,1)(1,1,1)
```
All voxels with z=0 (Morton indices 0-3 in our 2×2×2 example) are stored contiguously
Then all voxels with z=1 (Morton indices 4-7) are stored contiguously
This pattern scales to larger volumes too:
In a 4×4×4 cube, indices 0-15 would be the entire z=0 layer
Indices 16-31 would be the z=1 layer
And so on
## Morton encoding
Morton encoding completes an entire z-layer before moving to the next one. This happens because the highest-order bit in the Morton code (the z bit in the most significant position) changes less frequently than the lower-order bits, creating this layered organization.
This property makes operations like rendering slice-by-slice more efficient, as entire z-layers are stored together.
The Morton encoding is applied at the chunk level (8×8×8)
It processes the entire front z-layer (z=0) first, traversing in a zig-zag pattern
The zig-zag pattern creates a space-filling curve that maintains spatial locality
After completing one z-layer, it moves to the next (z=1)
This continues through all 8 possible z-layers in the chunk
The location table (lc) uses this Morton ordering to efficiently address all possible chunks, and within each chunk, the data follows this same pattern when decoding positions.
This approach is particularly efficient for this voxel format because:
It groups related voxels together in memory
It enables quick access to entire z-layers
It maintains spatial locality which improves cache performance
## Location table chunks
The 256 bytes in the lc table doesn't represent voxels within a chunk, but rather which chunks exist in the entire volume:
Each byte in the 256-byte lc array corresponds to a potential chunk position
For a full 256×256×256 voxel world, you'd have 32×32×32 = 32,768 possible chunks
The 256-byte limit means each snapshot can only reference up to 256 distinct chunks
This is why the format uses multiple snapshots - it allows the format to build complex models by combining multiple snapshots, each referencing up to 256 chunks.
So while 256 bytes is small for individual storage, it's actually a limitation on how many chunks can be referenced in a single snapshot. For most practical voxel models, this is adequate since they typically use far fewer than 256 chunks.
In each snapshot:
The cid (Chunk ID) field identifies which specific 8×8×8 chunk was modified
The s dictionary contains the actual edit data:
lc (Location Table): Shows which voxels in the chunk were edited
ds (Data Stream): Contains the color values for those edited voxels
Each snapshot represents a single edit operation affecting one 8×8×8 chunk. The format builds up complex models by combining multiple snapshots, each modifying different chunks or the same chunks at different times.
Later snapshots override earlier ones when they modify the same voxel positions, allowing for an edit history that reflects the model's evolution.
## chunk ID
The cid (Chunk ID) has a range of 0-255 to match the addressing capacity of the lc table.
This means:
- Each snapshot can identify one specific chunk via its cid
- Can reference up to 256 unique chunks across all snapshots
- This 0-255 range aligns with the Morton encoding scheme for chunk coordinates and provides a clean 8-bit identifier for each chunk.
## Morton encoding on the chunks
The chunk indices use Morton encoding (Z-order curve), just like the voxel positions within chunks.
To convert from a cid in range 0-255 to chunk coordinates in an 8×8×8 chunk volume:
Convert the index to binary (8 bits)
### Deinterleave the bits:
Extract bits 0, 3, 6 for the x coordinate
Extract bits 1, 4, 7 for the y coordinate
Extract bits 2, 5 for the z coordinate (note: z only needs 2 bits for 0-3 range)
For example, to convert chunk ID 73 to 3D chunk coordinates:
73 in binary: 01001001
### Deinterleaving:
```
x = bits 0,3,6 = 101 = 5
y = bits 1,4,7 = 001 = 1
z = bits 2,5 = 00 = 0
```
So chunk ID 73 corresponds to chunk position (5,1,0).
The Morton encoding ensures that spatially close chunks are also close in memory.

View File

@ -10,10 +10,14 @@ ifeq ($(UNAME), Darwin)
LZFSELIBNAME = liblzfse.dylib
INCLUDEDIRS = -I$(SDKBASE)/src
INCLUDEDIRS2 = -I../lzfse/src
INCLUDEDIRS3 = -I../PlistCpp/include
INCLUDEDIRS4 = -I../PlistCpp/src
LIBDIR = $(SDKBASE)/lib
LIBDIRS = -L$(LIBDIR)
LZFSEBUILDDIR = ../lzfse/build
LZFSELIBDIR = -L$(LZFSEBUILDDIR)
PLISTDIR = ../PlistCpp/build
PLISTLIBDIR = -L$(PLISTDIR)
OBJDIR = obj/$(UNAME)
BINDIR = bin/$(UNAME)
OUTPUT = $(BINDIR)/$(OUTNAME)
@ -30,13 +34,16 @@ ifeq ($(UNAME), Darwin)
-fvisibility=hidden\
-O3\
$(INCLUDEDIRS)\
$(INCLUDEDIRS2)
$(INCLUDEDIRS2)\
$(INCLUDEDIRS3)\
$(INCLUDEDIRS4)
CFLAGS = $(CCFLAGS)\
-std=c17
CXXFLAGS = $(CCFLAGS)\
-std=c++17
-std=c++17\
-Wno-deprecated-declarations
CPPDEFINES = -DNDEBUG=1\
-DDL_USE_SHARED
@ -44,7 +51,8 @@ ifeq ($(UNAME), Darwin)
LIBS = -l$(SDKNAME)\
-lm\
-ldl\
-llzfse
-llzfse\
$(PLISTDIR)/libplist.a
LINKFLAGS = -mmacosx-version-min=11.0\
-isysroot $(ISYSROOT)\
@ -61,8 +69,14 @@ else
SDKFNAME2 = liblzfse.so
INCLUDEDIRS = -I$(SDKBASE)/src
INCLUDEDIRS2 = -I../lzfse/src
INCLUDEDIRS3 = -I../PlistCpp/include
INCLUDEDIRS4 = -I../PlistCpp/src
LIBDIR = $(SDKBASE)/lib
LIBDIRS = -L$(LIBDIR)
LZFSEBUILDDIR = ../lzfse/build
LZFSELIBDIR = -L$(LZFSEBUILDDIR)
PLISTDIR = ../PlistCpp/build
PLISTLIBDIR = -L$(PLISTDIR)
OBJDIR = obj/$(UNAME)
BINDIR = bin/$(UNAME)
OUTPUT = $(BINDIR)/$(OUTNAME)
@ -76,7 +90,9 @@ else
-D_FILE_OFFSET_BITS=64\
-O3\
$(INCLUDEDIRS)\
$(INCLUDEDIRS2)
$(INCLUDEDIRS2)\
$(INCLUDEDIRS3)\
$(INCLUDEDIRS4)
CFLAGS = $(CCFLAGS)\
@ -93,7 +109,8 @@ else
-ldl\
-lrt\
-lpthread\
-llzfse
-llzfse\
$(PLISTDIR)/libplist.a
LINKFLAGS = -m64\
-fvisibility=hidden\
@ -102,16 +119,19 @@ else
-Wl,-rpath,'$$ORIGIN/lib'
endif
OBJS = vmax2bella.o
OBJS = vmax2bella.o
OBJ = $(patsubst %,$(OBJDIR)/%,$(OBJS))
# Define the path to the precompiled pugixml.o
PUGIXML_OBJ = pugixml/bin/$(UNAME)/pugixml.o
$(OBJDIR)/%.o: %.cpp
@mkdir -p $(@D)
$(CXX) -c -o $@ $< $(CXXFLAGS) $(CPPDEFINES)
$(OUTPUT): $(OBJ)
$(OUTPUT): $(OBJ) $(PUGIXML_OBJ)
@mkdir -p $(@D)
$(CXX) -o $@ $^ $(LINKFLAGS) $(LIBDIRS) $(LZFSELIBDIR) $(LIBS)
$(CXX) -o $@ $(OBJ) $(PUGIXML_OBJ) $(LINKFLAGS) $(LIBDIRS) $(LZFSELIBDIR) $(LIBS)
@cp $(LIBDIR)/$(SDKFNAME) $(BINDIR)/$(SDKFNAME)
@cp $(LZFSEBUILDDIR)/$(LZFSELIBNAME) $(BINDIR)/$(LZFSELIBNAME)

File diff suppressed because it is too large Load Diff