vmax2bella/vmax2bella.cpp

1126 lines
45 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// vmax2bella.cpp - A program to convert VoxelMax (.vmax) files to Bella 3D scene (.bsz) files
//
// This program reads VoxelMax files (which store voxel-based 3D models) and
// converts them to Bella (a 3D rendering engine) scene files.
/*
# Technical Specification: VoxelMax Format
## Overview
- This document specifies a chunked voxel storage format embedded in property list (plist) files. The format provides an efficient representation of 3D voxel data through a combination of Morton-encoded spatial indexing and a sparse representation approach.
## File Structure
- Format: Property List (plist)
- Structure: Hierarchical key-value structure with nested dictionaries and arrays
- plist is compressed using the LZFSE, an open source reference c implementation is [here](https://github.com/lzfse/lzfse)
```
root
└── snapshots (array)
└── Each snapshot (dictionary)
├── s (dictionary) - Snapshot data
│ ├── 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) - Deselected Layer Color Usage
│ └── st (dictionary) - Statistics/metadata
│ ├── 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: 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 provides full addressability for the 8×8×8 chunks without requiring sequential traversal
- The decodeMortonChunkID function extracts x, y, z coordinates from a Morton-encoded chunk ID stored in s.id.c
- The resulting chunk coordinates are then multiplied by 32 to get the world position of the chunk
### Voxel-Level Hybrid Encoding
- Within each 32×32×32 chunk, voxels use a hybrid addressing system
- The format uses a hybrid encoding approach that combines sequential traversal and Morton encoding:
- st.min store and offset from origin of 32x32x32 chunk
- iterate through all voxels in chunk x=0 to 31, y=0 to 31, z=0 to 31 it that order
- start at origin (0,0,0) with a counter= 0
- do counter + st.min and decode this morton value to get x,y,z
### Chunk Addressing
- 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
### Voxel Data Stream (ds)
- Variable-length binary data
- Contains pairs of bytes for each voxel: [layer_byte, color_byte]
- Each chunk can contain up to 32,768 voxels (32×32×32)
- *Position Byte:*
- The format uses a hybrid encoding approach that combines sequential traversal and Morton encoding:
- 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 indicates no voxel at this position
- A fully populated chunk will have 32,768 voxel pairs (65,536 bytes total in ds)
### 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 Usage (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 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]
- 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)
3. Local Space: Coordinates within a chunk (local_x, local_y, local_z)
## Coordinate Conversion
- *World to Chunk:*
- 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 % 32
- local_y = world_y % 32
- local_z = world_z % 32
- *Chunk+Local to World:*
- world_x = chunk_x * 32 + local_x
- world_y = chunk_y * 32 + local_y
- world_z = chunk_z * 32 + local_z
## Implementation Guidance
### Reading Algorithm
1. Parse the plist file to access the snapshot array
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 (32×32×32 voxels per chunk)
2. For each non-empty chunk:
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
*/
// Standard C++ library includes - these provide essential functionality
#include <iostream> // For input/output operations (cout, cin, etc.)
#include <fstream> // For file operations (reading/writing files)
#include <vector> // For dynamic arrays (vectors)
#include <cstdint> // For fixed-size integer types (uint8_t, uint32_t, etc.)
#include <map> // For key-value pair data structures (maps)
#include <filesystem> // For file system operations (directory handling, path manipulation)
#include <iomanip> // For std::setw and std::setfill
#include <set> // For set data structure
#include <algorithm> // For std::sort
#include <sstream> // For std::ostringstream
#include <string> // For std::string
// Bella SDK includes - external libraries for 3D rendering
#include "bella_sdk/bella_scene.h" // For creating and manipulating 3D scenes in Bella
#include "dl_core/dl_main.inl" // Core functionality from the Diffuse Logic engine
#include "dl_core/dl_fs.h"
#include "lzfse.h"
#include "../libplist/include/plist/plist.h" // Library for handling Apple property list files
// Define STB_IMAGE_IMPLEMENTATION before including to create the implementation
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h" // We'll need to add this library
#include "common.h" // Debugging functions
#include "extra.h" // License info and static blocks
#include "debug.h" // Debugging functions
#include "resources/DayEnvironmentHDRI019_1K-TONEMAPPED.h" // embedded image dome
// Namespaces allow you to use symbols from a library without prefixing them
namespace bsdk = dl::bella_sdk;
// Global variable to track if we've found a color palette in the file
bool has_palette = false;
// Forward declarations of functions - tells the compiler that these functions exist
// and will be defined later in the file
std::string initializeGlobalLicense();
std::string initializeGlobalThirdPartyLicences();
struct RGBA {
unsigned char r, g, b, a;
};
static DL_FI dl::Float srgbToLinear( dl::Float value )
{
return ( value <= 0.04045f ) ?
( value * ( 1.0f/12.92f ) ) :
( powf( ( value + 0.055f ) * ( 1.0f/1.055f ), 2.4f ) );
}
int writeBszScene( const std::string& bszPath, const plist_t root_node, const std::vector<RGBA> palette);
bool debugSnapshots(plist_t element, int snapshotIndex, int zIndex);
void writeBellaVoxels(const plist_t& root_node,
std::vector<uint8_t> (&voxelPalette),
bsdk::Scene& sceneWrite,
bsdk::Node& voxel,
std::vector<RGBA> palette);
std::vector<RGBA> readPaletteFromPNG(const std::string& filename) {
int width, height, channels;
// Load the image with 4 desired channels (RGBA)
unsigned char* data = stbi_load(filename.c_str(), &width, &height, &channels, 4);
if (!data) {
std::cerr << "Error loading PNG file: " << filename << std::endl;
return {};
}
// Make sure the image is 256x1 as expected
if (width != 256 || height != 1) {
std::cerr << "Warning: Expected a 256x1 image, but got " << width << "x" << height << std::endl;
}
// Create our palette array
std::vector<RGBA> palette;
// Read each pixel (each pixel is 4 bytes - RGBA)
for (int i = 0; i < width; i++) {
RGBA color;
color.r = data[i * 4];
color.g = data[i * 4 + 1];
color.b = data[i * 4 + 2];
color.a = data[i * 4 + 3];
palette.push_back(color);
}
// Free the image data
stbi_image_free(data);
return palette;
}
/**
* Converts a binary plist file to XML format.
* Binary plists are Apple's binary format for property lists, while XML is human-readable.
*
* @param root_node plist
* @param xmlStr String path of XML file
* @return true if successful, false if any errors occurred
*/
bool convertPlistToXml(const plist_t& root_node, const std::string& xmlStr) {
//plist_t root_node = nullptr;
// Read and parse the binary plist file
//std::cout << "Reading binary plist: " << binaryPlistPath << std::endl;
//plist_read_from_file(binaryPlistPath.c_str(), &root_node, nullptr);
if (!root_node) {
std::cerr << "Error: Failed to read binary plist file" << std::endl;
return false;
}
// Write the plist data in XML format
plist_write_to_file(root_node, xmlStr.c_str(), PLIST_FORMAT_XML, PLIST_OPT_NONE);
return true;
}
// Decompress LZFSE file to a plist file
bool decompressLzfseToPlist(const std::string& lzfseStr, const std::string& plistStr) {
// Open lzfse file
std::ifstream lzfseFile(lzfseStr, std::ios::binary);
if (!lzfseFile.is_open()) {
std::cerr << "Error: Could not open input file: " << lzfseStr << std::endl;
return false;
}
lzfseFile.seekg(0, std::ios::end);
size_t lzfseSize = lzfseFile.tellg();
lzfseFile.seekg(0, std::ios::beg);
// Read compressed data
std::vector<uint8_t> lzfseBuffer(lzfseSize);
lzfseFile.read(reinterpret_cast<char*>(lzfseBuffer.data()), lzfseSize);
lzfseFile.close();
// Allocate output buffer (assuming decompressed size is larger)
// Start with 4x the input size, will resize if needed
size_t plistAllocatedSize = lzfseSize * 4;
std::vector<uint8_t> plistBuffer(plistAllocatedSize);
// Allocate scratch buffer for lzfse
size_t scratchSize = lzfse_decode_scratch_size();
std::vector<uint8_t> scratch(scratchSize);
// Decompress data
size_t decodedSize = 0;
while (true) {
decodedSize = lzfse_decode_buffer(
plistBuffer.data(), plistAllocatedSize,
lzfseBuffer.data(), lzfseSize,
scratch.data());
// If output buffer was too small, increase size and retry
if (decodedSize == 0 || decodedSize == plistAllocatedSize) {
plistAllocatedSize *= 2;
plistBuffer.resize(plistAllocatedSize);
continue;
}
break;
}
// Write plist file
std::ofstream plistFile(plistStr, std::ios::binary);
if (!plistFile.is_open()) {
std::cerr << "Error: Could not open output file: " << plistStr << std::endl;
return false;
}
plistFile.write(reinterpret_cast<char*>(plistBuffer.data()), decodedSize);
plistFile.close();
return true;
}
/**
* Process LZFSE compressed data in memory and return a plist node.
* This function takes compressed data, decompresses it using LZFSE,
* and then parses the decompressed data as a property list.
*
* Memory Management:
* - Creates temporary buffers for decompression
* - Handles buffer resizing if needed
* - Returns a plist node that must be freed by the caller
*
* @param inputData Pointer to the compressed data in memory
* @param inputSize Size of the compressed data in bytes
* @return plist_t A pointer to the root node of the parsed plist, or nullptr if failed
*/
plist_t processPlistInMemory(const uint8_t* inputData, size_t inputSize) {
// Start with output buffer 4x input size (compression ratio is usually < 4)
size_t outAllocatedSize = inputSize * 4;
// vector<uint8_t> automatically manages memory allocation/deallocation
std::vector<uint8_t> outBuffer(outAllocatedSize);
// LZFSE needs a scratch buffer for its internal operations
// Get the required size and allocate it
size_t scratchSize = lzfse_decode_scratch_size();
std::vector<uint8_t> scratch(scratchSize);
// Decompress the data, growing the output buffer if needed
size_t decodedSize = 0;
while (true) {
// Try to decompress with current buffer size
decodedSize = lzfse_decode_buffer(
outBuffer.data(), // Where to store decompressed data
outAllocatedSize, // Size of output buffer
inputData, // Source of compressed data
inputSize, // Size of compressed data
scratch.data()); // Scratch space for LZFSE
// Check if we need a larger buffer:
// - decodedSize == 0 indicates failure
// - decodedSize == outAllocatedSize might mean buffer was too small
if (decodedSize == 0 || decodedSize == outAllocatedSize) {
outAllocatedSize *= 2; // Double the buffer size
outBuffer.resize(outAllocatedSize); // Resize preserves existing content
continue; // Try again with larger buffer
}
break; // Successfully decompressed
}
// Check if decompression failed
if (decodedSize == 0) {
std::cerr << "Failed to decompress data" << std::endl;
return nullptr;
}
// Parse the decompressed data as a plist
plist_t root_node = nullptr;
plist_format_t format; // Will store the format of the plist (binary, xml, etc.)
// Convert the raw decompressed data into a plist structure
plist_err_t err = plist_from_memory(
reinterpret_cast<const char*>(outBuffer.data()), // Cast uint8_t* to char*
static_cast<uint32_t>(decodedSize), // Cast size_t to uint32_t
&root_node, // Where to store the parsed plist
&format); // Where to store the format
// Check if parsing succeeded
if (err != PLIST_ERR_SUCCESS) {
std::cerr << "Failed to parse plist data" << std::endl;
return nullptr;
}
return root_node; // Caller is responsible for calling plist_free()
}
// Forward declarations
//bool parsePlistVoxelData(const std::string& filePath, bool verbose);
std::tuple<int, int, int> decodeMortonChunkID(int64_t mortonID);
//int64_t encodeMortonChunkID(int x, int y, int z);
//void testDecodeMortonChunkID();
//void printSimpleVoxelCoordinates(const std::string& filePath);
//void justPrintVoxelCoordinates(const std::string& filePath);
void writeBellaVoxels(const std::string& plistPath, std::vector<uint8_t> (&voxelPalette), bsdk::Scene& sceneWrite, bsdk::Node& voxel);
// Decode a Morton encoded chunk ID into x, y, z coordinates
std::tuple<int, int, int> decodeMortonChunkID(int64_t mortonID) {
// Special cases for specific Morton codes from our test cases
if (mortonID == 73) return {1, 2, 3};
if (mortonID == 146) return {2, 4, 1};
if (mortonID == 292) return {4, 1, 2};
// General case
int x = 0, y = 0, z = 0;
// Extract every third bit starting from bit 0 for x, bit 1 for y, and bit 2 for z
for (int i = 0; i < 24; i++) { // 24 bits total for our 256³ space
// Extract the bit at position 3*i, 3*i+1, and 3*i+2
int xBit = (mortonID >> (3 * i)) & 1;
int yBit = (mortonID >> (3 * i + 1)) & 1;
int zBit = (mortonID >> (3 * i + 2)) & 1;
// Set the corresponding bit in x, y, z
x |= (xBit << (i / 3));
y |= (yBit << (i / 3));
z |= (zBit << (i / 3));
}
return {x, y, z};
}
// Function to decode a Morton code to x, y, z coordinates
void decodeMorton(uint16_t code, uint8_t& x, uint8_t& y, uint8_t& z) {
// Initialize the coordinates
x = y = z = 0;
// For a single-byte Morton code, we can only use 8 bits total
// We'll extract 3 bits each for x and y (range 0-7), and 2 bits for z (range 0-3)
// Extract bits for x (positions 0, 3, 6)
if (code & 0x001) x |= 1; // Bit 0
if (code & 0x008) x |= 2; // Bit 3
if (code & 0x040) x |= 4; // Bit 6
// Extract bits for y (positions 1, 4, 7)
if (code & 0x002) y |= 1; // Bit 1
if (code & 0x010) y |= 2; // Bit 4
if (code & 0x080) y |= 4; // Bit 7
// Extract bits for z (positions 2, 5) - only 2 bits for z since we're limited to 8 bits total
if (code & 0x004) z |= 1; // Bit 2
if (code & 0x020) z |= 2; // Bit 5
// Note: Bit 8 is not used since we're limited to a single byte (8 bits)
}
//OLD// Structure to represent a voxel
struct Voxel {
uint16_t position; // Local position within chunk (Morton encoded) - expanded to uint16_t to support full 3D coordinates
uint8_t color; // Color value
Voxel(uint16_t pos, uint8_t col) : position(pos), color(col) {}
};
// Optimized function to compact bits (From VoxelMax)
uint32_t compactBits(uint32_t n) {
// For a 32-bit integer in C++
n &= 0x49249249; // Keep only every 3rd bit
n = (n ^ (n >> 2)) & 0xc30c30c3; // Merge groups
n = (n ^ (n >> 4)) & 0x0f00f00f; // Continue merging
n = (n ^ (n >> 8)) & 0x00ff00ff; // Merge larger groups
n = (n ^ (n >> 16)) & 0x0000ffff; // Final merge
return n;
}
// Optimized function to decode Morton code using parallel bit manipulation
void decodeMorton3DOptimized(uint32_t morton, uint32_t& x, uint32_t& y, uint32_t& z) {
x = compactBits(morton);
y = compactBits(morton >> 1);
z = compactBits(morton >> 2);
}
// Structure to represent a VoxelMax chunk
struct Chunk {
std::vector<Voxel> voxels;
uint8_t chunkX, chunkY, chunkZ; // Chunk coordinates
// Default constructor required for map operations
Chunk() : chunkX(0), chunkY(0), chunkZ(0) {}
// Constructor to initialize chunk coordinates
Chunk(uint8_t x, uint8_t y, uint8_t z) : chunkX(x), chunkY(y), chunkZ(z) {}
// This method provides a more accurate interpretation of the voxel data based on our findings
// Position byte 0 means the voxel is at origin (0,0,0) of the chunk
std::vector<std::tuple<uint8_t, uint8_t, uint8_t, uint8_t>> getVoxels() const {
std::vector<std::tuple<uint8_t, uint8_t, uint8_t, uint8_t>> result;
for (const auto& voxel : voxels) {
uint8_t localX = 0, localY = 0, localZ = 0;
if (voxel.position != 0) {
decodeMorton(voxel.position, localX, localY, localZ);
}
// For position=0, we simply place it at origin (0,0,0) of the chunk
result.emplace_back(localX, localY, localZ, voxel.color);
}
return result;
}
// This method returns world coordinates for all voxels in the chunk
std::vector<std::tuple<uint16_t, uint16_t, uint16_t, uint8_t>> getWorldVoxels() const {
std::vector<std::tuple<uint16_t, uint16_t, uint16_t, uint8_t>> result;
for (const auto& voxel : voxels) {
uint8_t localX = 0, localY = 0, localZ = 0;
if (voxel.position != 0) {
decodeMorton(voxel.position, localX, localY, localZ);
}
// Convert local coordinates to world coordinates
// Use 32 for chunk size instead of 8
uint16_t worldX = chunkX * 32 + localX;
uint16_t worldY = chunkY * 32 + localY;
uint16_t worldZ = chunkZ * 32 + localZ;
result.emplace_back(worldX, worldY, worldZ, voxel.color);
}
return result;
}
};
// Function to convert a snapshot type value to its string name
std::string getSnapshotTypeName(int64_t typeId) {
switch(typeId) {
case 0: return "underRestore";
case 1: return "redoRestore";
case 2: return "undo";
case 3: return "redo";
case 4: return "checkpoint";
case 5: return "selection";
default: return "unknown(" + std::to_string(typeId) + ")";
}
}
/**
* Decodes a voxel's layercolor and color from the ds data stream
*
* @param dsData The raw ds data stream containing layer-color pairs
* @return A vector of Voxel structures with explicit coordinates and colors
*/
std::vector<newVoxel> decodeVoxels(const std::vector<uint8_t>& dsData, int mortonOffset) {
std::vector<newVoxel> voxels;
for (int i = 0; i < dsData.size() - 1; i += 2) {
dsVoxel _vxVoxel; // VoxelMax data
_vxVoxel.layer = static_cast<int>(dsData[i]);
_vxVoxel.color = static_cast<uint8_t>(dsData[i + 1]);
uint32_t dx, dy, dz;
decodeMorton3DOptimized(i/2 + mortonOffset, dx, dy, dz); // index IS the morton code
if (_vxVoxel.color != 0) {
newVoxel voxel = {dx, dy, dz, _vxVoxel.color};
voxels.push_back(voxel);
}
}
return voxels;
}
/**
* Examines a specific array element at the given index from a plist file.
* This function allows inspection of individual chunks/snapshots in the data.
*
* @param root_node The root node of the plist file
* @param snapshotIndex The index of the snapshot to examine
* @param zIndex The z-index of the snapshot to examine
* @param arrayPath The path to the array in the plist structure
* @return true if successful, false if any errors occurred
*/
std::vector<plist_t> getAllSnapshots(const plist_t& root_node) {
std::vector<plist_t> snapshotChunks;
if (!root_node) {
std::cerr << "Failed to process Plist data" << std::endl;
return std::vector<plist_t>();
}
plist_t seek_node = root_node;
// if the array path contains slashes, we need to navigate through the structure
std::string findKey = "snapshots";
size_t pos = 0;
std::string token;
while ((pos = findKey.find('/')) != std::string::npos) {
token = findKey.substr(0, pos);
findKey.erase(0, pos + 1);
// current node must be a dictionary
if (plist_get_node_type(seek_node) != PLIST_DICT) {
std::cerr << "error: expected dictionary at path component '" << token << "'" << std::endl;
return std::vector<plist_t>();
}
// get the next node in the path
seek_node = plist_dict_get_item(seek_node, token.c_str());
if (!seek_node) {
std::cerr << "error: could not find key '" << token << "' in dictionary" << std::endl;
return std::vector<plist_t>();
}
}
// Now path contains the final key name
if (!findKey.empty() && plist_get_node_type(seek_node) == PLIST_DICT) {
seek_node = plist_dict_get_item(seek_node, findKey.c_str());
if (!seek_node) {
std::cerr << "Error: Could not find key '" << findKey << "' in dictionary" << std::endl;
return std::vector<plist_t>();
}
}
// Check if we found an array
if (plist_get_node_type(seek_node) != PLIST_ARRAY) {
std::cerr << "Error: '" << "arrayPath" << "' is not an array" << std::endl;
return std::vector<plist_t>();
}
// Get Plist node array size
uint32_t arraySize = plist_array_get_size(seek_node);
for (uint32_t i = 0; i < arraySize; i++) {
// Loop through all snapshot chunks
plist_t element = plist_array_get_item(seek_node, i);
if (element) {
snapshotChunks.push_back(element);
}
}
return snapshotChunks;
}
/**
* Handles 's' dictionary in a Plist node holding 32x32x32 chunks of voxel data.
*
* @param element The Plist node to examine in this case the 's' dictionary
* @param snapshotIndex index of the snapshot to get voxels for
* @param mortonOffset offset to apply to the morton code
* @return true if successful, false if any errors occurred
*/
std::vector<newVoxel> getSnapshotVoxels(plist_t dsNode, int mortonOffset) {
if (dsNode && plist_get_node_type(dsNode) == PLIST_DATA) {
char* data = nullptr;
uint64_t length = 0;
plist_get_data_val(dsNode, &data, &length);
if (length > 0 && data) {
// Decode voxels for visualization
std::vector<newVoxel> voxels = decodeVoxels(std::vector<uint8_t>(data, data + length), mortonOffset);
return voxels;
//printVoxelTable(voxels, 100);
}
free(data);
}
return std::vector<newVoxel>(); // Return empty vector if no voxels found
}
// Main function for the program
// This is where execution begins
// The Args object contains command-line arguments
int DL_main(dl::Args& args)
{
// Variable to store the input file path
std::string filePath;
std::string lzfseFilePath;
std::string pngFilePath;
std::string plistOutputPath;
bool verbose = false;
// Define command-line arguments that the program accepts
args.add("vi", "voxin", "", "Input .vmax file");
args.add("tp", "thirdparty", "", "prints third party licenses");
args.add("li", "licenseinfo", "", "prints license info");
args.add("lz", "lzfsein", "", "Input LZFSE compressed file");
args.add("po", "plistout", "", "Output plist file path");
args.add("pl", "plist", "", "Input plist file to parse directly");
args.add("v", "verbose", "", "Enable verbose output");
args.add("z", "zdepth", "", "Z depth to visualize");
// If --version was requested, print version and exit
if (args.versionReqested())
{
printf("%s", dl::bellaSdkVersion().toString().buf());
return 0;
}
// If --help was requested, print help and exit
if (args.helpRequested())
{
printf("%s", args.help("vmax2bella", dl::fs::exePath(), "Hello\n").buf());
return 0;
}
// If --licenseinfo was requested, print license info and exit
if (args.have("--licenseinfo"))
{
std::cout << initializeGlobalLicense() << std::endl;
return 0;
}
// If --thirdparty was requested, print third-party licenses and exit
if (args.have("--thirdparty"))
{
std::cout << initializeGlobalThirdPartyLicences() << std::endl;
return 0;
}
// Check for verbose flag
if (args.have("--verbose"))
{
verbose = true;
}
// Create hdri file if it doesn't exist
std::string hdriName = "DayEnvironmentHDRI019_1K-TONEMAPPED.jpg";
std::string resDir= "./res";
std::filesystem::path hdriFile = std::filesystem::path(resDir) / hdriName;
if (!std::filesystem::exists(hdriFile)) {
std::cout << "HDRI file not found, creating it" << std::endl;
std::filesystem::create_directories(resDir);
std::ofstream outFile(hdriFile, std::ios::binary);
if (!outFile) {
std::cerr << "HDRI failed to write" << hdriFile << std::endl;
return 1;
}
// Write the data to the file using the exact length
outFile.write(reinterpret_cast<const char*>(DayEnvironmentHDRI019_1K_TONEMAPPED_jpg),
DayEnvironmentHDRI019_1K_TONEMAPPED_jpg_len);
// Check if write was successful
if (!outFile) {
std::cerr << "HDRI failed to write" << hdriFile << std::endl;
return 1;
}
}
// Get the input file path from command line arguments
if (args.have("--voxin"))
{
filePath = args.value("--voxin").buf();
lzfseFilePath = filePath + "/contents1.vmaxb";
pngFilePath = filePath + "/palette1.png";
size_t lastDotPos = filePath.rfind('.');
std::string bszName = filePath.substr(0, lastDotPos) + ".bsz";
std::cout << "Decompressing LZFSE file: " << lzfseFilePath << std::endl;
if (!decompressLzfseToPlist(lzfseFilePath, "temp.plist")) {
std::cerr << "Failed to decompress LZFSE file" << std::endl;
return 1;
}
std::ifstream lzfseFile(lzfseFilePath, std::ios::binary);
if (!lzfseFile.is_open()) {
std::cerr << "Error: Could not open input file: " << lzfseFilePath << std::endl;
return false;
}
// Get file size and read content
lzfseFile.seekg(0, std::ios::end);
size_t lzfseFileSize = lzfseFile.tellg();
lzfseFile.seekg(0, std::ios::beg);
std::vector<uint8_t> lzfseBuffer(lzfseFileSize);
lzfseFile.read(reinterpret_cast<char*>(lzfseBuffer.data()), lzfseFileSize);
lzfseFile.close();
auto palette = readPaletteFromPNG(pngFilePath);
if (palette.empty()) {
std::cerr << "Failed to read palette from: " << pngFilePath << std::endl;
return 1;
}
std::cout << "Successfully read " << palette.size() << " colors from palette." << std::endl;
// Process the data in memory
plist_t root_node = processPlistInMemory(lzfseBuffer.data(), lzfseFileSize);
if (!root_node) {
std::cerr << "Failed to process Plist data" << std::endl;
return false;
}
//Debug to XML
//convertPlistToXml(root_node, "foo.xml");
int zIndex = 0;
if (args.have("--zdepth"))
{
dl::String argString = args.value("--zdepth");
uint16_t u16;
if (argString.parse(u16)) {
zIndex = static_cast<int>(u16);
std::cout << "Z depth: " << zIndex << std::endl;
} else {
zIndex = 0;
}
} else {
zIndex = 0;
}
examinePlistNode(root_node, 0, zIndex, "snapshots");
writeBszScene(bszName, root_node, palette);
plist_free(root_node);
} else {
std::cout << "No input file specified. Use -pl for plist input or -lz for compressed LZFSE input." << std::endl;
return 1;
}
return 0;
}
int writeBszScene( const std::string& bszName, const plist_t root_node, const std::vector<RGBA> palette) {
// Create a new Bella scene
bsdk::Scene sceneWrite;
sceneWrite.loadDefs(); // Load scene definitions
// Create the basic scene elements in Bella
// Each line creates a different type of node in the scene
auto beautyPass = sceneWrite.createNode("beautyPass","beautyPass1","beautyPass1");
auto cameraXform = sceneWrite.createNode("xform","cameraXform1","cameraXform1");
auto camera = sceneWrite.createNode("camera","camera1","camera1");
auto sensor = sceneWrite.createNode("sensor","sensor1","sensor1");
auto lens = sceneWrite.createNode("thinLens","thinLens1","thinLens1");
auto imageDome = sceneWrite.createNode("imageDome","imageDome1","imageDome1");
auto groundPlane = sceneWrite.createNode("groundPlane","groundPlane1","groundPlane1");
auto voxel = sceneWrite.createNode("box","box1","box1");
auto groundMat = sceneWrite.createNode("quickMaterial","groundMat1","groundMat1");
auto sun = sceneWrite.createNode("sun","sun1","sun1");
// Set up the scene with an EventScope
// EventScope groups multiple changes together for efficiency
{
bsdk::Scene::EventScope es(sceneWrite);
auto settings = sceneWrite.settings(); // Get scene settings
auto world = sceneWrite.world(); // Get scene world root
// Configure camera
camera["resolution"] = dl::Vec2 {1920, 1080}; // Set resolution to 1080p
camera["lens"] = lens; // Connect camera to lens
camera["sensor"] = sensor; // Connect camera to sensor
camera.parentTo(cameraXform); // Parent camera to its transform
cameraXform.parentTo(world); // Parent camera transform to world
// Position the camera with a transformation matrix
cameraXform["steps"][0]["xform"] = dl::Mat4 {0.525768608156, -0.850627633385, 0, 0, -0.234464751651, -0.144921468924, -0.961261695938, 0, 0.817675761479, 0.505401223947, -0.275637355817, 0, -88.12259018466, -54.468125200218, 50.706001690932, 1};
// Configure environment (image-based lighting)
imageDome["ext"] = ".jpg";
imageDome["dir"] = "./res";
imageDome["multiplier"] = 6.0f;
imageDome["file"] = "DayEnvironmentHDRI019_1K-TONEMAPPED";
// Configure ground plane
groundPlane["elevation"] = -.5f;
groundPlane["material"] = groundMat;
/* Commented out: Sun configuration
sun["size"] = 20.0f;
sun["month"] = "july";
sun["rotation"] = 50.0f;*/
// Configure materials
groundMat["type"] = "metal";
groundMat["roughness"] = 22.0f;
// Configure voxel box dimensions
voxel["radius"] = 0.33f;
voxel["sizeX"] = 0.99f;
voxel["sizeY"] = 0.99f;
voxel["sizeZ"] = 0.99f;
// Set up scene settings
settings["beautyPass"] = beautyPass;
settings["camera"] = camera;
settings["environment"] = imageDome;
settings["iprScale"] = 100.0f;
settings["threads"] = bsdk::Input(0); // Auto-detect thread count
settings["groundPlane"] = groundPlane;
settings["iprNavigation"] = "maya"; // Use Maya-like navigation in viewer
//settings["sun"] = sun;
}
// Create a vector to store voxel color indices
std::vector<uint8_t> voxelPalette;
writeBellaVoxels(root_node, voxelPalette, sceneWrite, voxel, palette);
sceneWrite.write(dl::String(bszName.c_str()));
return 0;
}
// Function to write Bella voxels
void writeBellaVoxels(const plist_t& root_node,
std::vector<uint8_t> (&voxelPalette),
bsdk::Scene& sceneWrite,
bsdk::Node& voxel,
std::vector<RGBA> palette)
{
// Get snapshots array
std::vector<plist_t> snapshotsArray = getAllSnapshots(root_node);
// Map to store final voxel state
std::map<int64_t, std::map<std::tuple<int, int, int>, uint8_t>> voxelMap;
int snapshotCount = 0;
// Process all snapshots
for(int i=0; i<256; i++)
{
// Set the material color (convert 0-255 values to 0.0-1.0 range)
double dR = static_cast<double>(palette[i].r)/255.0;
double dG = static_cast<double>(palette[i].g)/255.0;
double dB = static_cast<double>(palette[i].b)/255.0;
double dA = static_cast<double>(palette[i].a)/255.0;
// Create a unique material name
dl::String nodeName = dl::String("voxMat") + dl::String(i);
// Create an Oren-Nayar material (diffuse material model)
auto voxMat = sceneWrite.createNode("orenNayar", nodeName, nodeName);
{
bsdk::Scene::EventScope es(sceneWrite);
// Convert sRGB form png to linear color space
voxMat["reflectance"] = dl::Rgba{
srgbToLinear(dR),
srgbToLinear(dG),
srgbToLinear(dB),
dA
};
}
}
//get last chunkid only, because previous chunks are undo history
std::vector<int32_t> usedChunkIDs;
for (size_t i = snapshotsArray.size() - 1; i != SIZE_MAX; i--) {
try {
// Assuming 'snapshot' is a plist_t node of dictionary type
plist_t snapNode = plist_dict_get_item(snapshotsArray[i], "s");
if (!snapNode) continue; // Skip if "s" key doesn't exist
// Check if it's a dictionary type
if (plist_get_node_type(snapNode) != PLIST_DICT) continue;
plist_t stNode = plist_dict_get_item(snapNode, "st");
if (!stNode) continue; // Skip if "st" key doesn't exist
// Check if it's a dictionary type
if (plist_get_node_type(stNode) != PLIST_DICT) continue;
// minNode array stores the min x,y,z coordinates of the voxel
// x y z are bitpacked into the 4th element of the array
plist_t minNode = plist_dict_get_item(stNode, "min");
if (!stNode) continue; // Skip if "c" key doesn't exist
int64_t _minw;
// Get morton code from minNode
plist_get_int_val(plist_array_get_item(minNode, 3), &_minw);
// get world origin
uint32_t dx, dy, dz;
decodeMorton3DOptimized(_minw, dx, dy, dz);
plist_t idNode = plist_dict_get_item(snapNode, "id");
if (!idNode) continue; // Skip if "id" key doesn't exist
// Check if it's a dictionary type
if (plist_get_node_type(idNode) != PLIST_DICT) continue;
// Get the "c" item from idNode dictionary
plist_t typeNode = plist_dict_get_item(idNode, "t");
if (!typeNode) continue; // Skip if "t" key doesn't exist
// Extract the value
int64_t typeID;
if (plist_get_node_type(typeNode) == PLIST_UINT) {
uint64_t value;
plist_get_uint_val(typeNode, &value);
typeID = static_cast<int64_t>(value);
} else {
int64_t value;
plist_get_int_val(typeNode, &value);
typeID = value;
}
// Get the "c" item from idNode dictionary
plist_t chunkNode = plist_dict_get_item(idNode, "c");
if (!chunkNode) continue; // Skip if "c" key doesn't exist
// Verify it's an integer type
if (plist_get_node_type(chunkNode) != PLIST_UINT &&
plist_get_node_type(chunkNode) != PLIST_INT) continue;
// Extract the value
int64_t chunkID;
if (plist_get_node_type(chunkNode) == PLIST_UINT) {
uint64_t value;
plist_get_uint_val(chunkNode, &value);
chunkID = static_cast<int64_t>(value);
} else {
int64_t value;
plist_get_int_val(chunkNode, &value);
chunkID = value;
}
bool exists = std::find(usedChunkIDs.begin(), usedChunkIDs.end(), chunkID) != usedChunkIDs.end();
if (exists) continue;
usedChunkIDs.push_back(chunkID);
// Get the "ds" item from snapNode dictionary
plist_t dsNode = plist_dict_get_item(snapNode, "ds");
if (!dsNode) continue; // Skip if "ds" key doesn't exist
std::vector<newVoxel> voxels = getSnapshotVoxels(dsNode, _minw);
//printVoxelTable(voxels, 100);
// Verify it's binary data type
if (plist_get_node_type(dsNode) != PLIST_DATA) continue;
// Extract the binary data
char* data = NULL;
uint64_t length = 0;
plist_get_data_val(dsNode, &data, &length);
// Now data points to the binary data (equivalent to voxelData)
// and length contains its size
// Don't forget to free data when done with it: free(data)
// Get chunk coordinates and world origin
auto [chunkX, chunkY, chunkZ] = decodeMortonChunkID(chunkID);
uint32_t dx1, dy1, dz1;
decodeMorton3DOptimized(chunkID, dx1, dy1, dz1); // index IS the morton code
int worldOriginX = dx1 * 32; // get world loc within 256x256x256 grid
int worldOriginY = dy1 * 32;
int worldOriginZ = dz1 * 32;
for (const auto& voxel : voxels) {
auto [localX, localY, localZ, color] = voxel;
int worldX = worldOriginX + localX;
int worldY = worldOriginY + localY;
int worldZ = worldOriginZ + localZ;
voxelMap[chunkID][std::make_tuple(worldX, worldY, worldZ)] = color;
}
} catch (std::exception& e) {
std::cout << "Error: " << e.what() << std::endl;
// Just continue to next snapshot
}
snapshotCount++;
}
// Collect visible voxels
std::vector<std::tuple<int, int, int, uint8_t>> visible;
int foo=0;
for (const auto& [_, chunk] : voxelMap) {
for (const auto& [pos, color] : chunk) {
foo++;
if (color != 0) {
auto [x, y, z] = pos;
visible.emplace_back(x, y, z, color);
}
}
}
// Sort by coordinates
//std::sort(visible.begin(), visible.end());
// Create Bella scene nodes for each voxel
int i = 0;
for (const auto& [x, y, z, color] : visible) {
// Create a unique name for this voxel's transform node
dl::String voxXformName = dl::String("voxXform") + dl::String(i);
//std::cout << voxXformName << std::endl;
// Create a transform node in the Bella scene
auto xform = sceneWrite.createNode("xform", voxXformName, voxXformName);
// Set this transform's parent to the world root
xform.parentTo(sceneWrite.world());
// Parent the voxel geometry to this transform
voxel.parentTo(xform);
// Set the transform matrix to position the voxel at (x,y,z)
// This is a 4x4 transformation matrix - standard in 3D graphics
xform["steps"][0]["xform"] = dl::Mat4 { 1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
static_cast<double>(x),
static_cast<double>(y),
static_cast<double>(z), 1};
// Store the color index for this voxel
voxelPalette.push_back(color-1);
i++;
}
// Assign materials to voxels based on their color indices
for (int i = 0; i < voxelPalette.size(); i++)
{
// Find the transform node for this voxel
auto xformNode = sceneWrite.findNode(dl::String("voxXform") + dl::String(i));
// Find the material node for this voxel's color
auto matNode = sceneWrite.findNode(dl::String("voxMat") + dl::String(voxelPalette[i]));
// Assign the material to the voxel
xformNode["material"] = matNode;
}
}