For a personal project I want to generate some 3D voxel stuff.
After looking around for a good file format candidate, I discovered Qubicle format which is easy to understand and accepted by many tools.
I wrote a simple Ruby script for it, I hope it may help someone in need for this kind of stuff.
Code is available under MIT license.
bloc.rb
# Represents a block
class Bloc
# @return [Integer]
attr_reader :r
# @return [Integer]
attr_reader :g
# @return [Integer]
attr_reader :b
# @return [Integer]
attr_reader :x
# @return [Integer]
attr_reader :y
# @return [Integer]
attr_reader :z
# @param [Integer] r_value
# @param [Integer] g_value
# @param [Integer] b_value
# @param [Integer] x_value
# @param [Integer] y_value
# @param [Integer] z_value
# @raise [RuntimeError] if a value is bad
def initialize(r_value, g_value, b_value, x_value, y_value, z_value)
check_color(r_value)
@r = r_value
check_color(g_value)
@g = g_value
check_color(b_value)
@b = b_value
check_position(x_value)
@x = x_value
check_position(y_value)
@y = y_value
check_position(z_value)
@z = z_value
end
def r=(r_value)
check_color(r_value)
@r = r_value
end
def g=(g_value)
check_color(g_value)
@g = g_value
end
def b=(b_value)
check_color(b_value)
@b = b_value
end
def x=(x_value)
check_position(x_value)
@x = x_value
end
def y=(y_value)
check_position(y_value)
@y = y_value
end
def z=(z_value)
check_position(z_value)
@z = z_value
end
private
# @param [Integer] value
# @return [void]
# @raise [RuntimeError] if a value is bad
def check_color(value)
raise "Value #{value} should be an integer" unless value.is_a? Integer
if value < 0
raise "Value #{value} should be >= 0"
elsif value > 254
raise "Value #{value} should be <= 254"
end
end
# @param [Integer] value
# @return [void]
# @raise [RuntimeError] if a value is bad
def check_position(value)
raise "Value #{value} should be an integer" unless value.is_a? Integer
if value < 0
raise "Value #{value} should be >= 0"
end
end
end
qubicle.rb
require 'logger'
require_relative 'bloc'
class Qubicle
MATRIX_NAME = 'all blocks'
LOGGER = Logger.new(STDOUT)
# @param [Array<Bloc>] blocs
# @param [IO] io
# @return [void]
def write(blocs, io)
LOGGER << "Will write #{blocs.length} blocks\n"
if blocs.empty?
raise 'Blocks list should not be empty'
end
add_header(io)
# Add blocks
first_bloc = blocs.first
min_x = first_bloc.x
max_x = first_bloc.x
min_y = first_bloc.y
max_y = first_bloc.y
min_z = first_bloc.z
max_z = first_bloc.z
# Find boundaries
blocs.each do |bloc|
x = bloc.x
if x > max_x
max_x = x
end
if x < min_x
min_x = x
end
y = bloc.y
if y > max_y
max_y = y
end
if y < min_y
min_y = y
end
z = bloc.z
if z > max_z
max_z = z
end
if z < min_z
min_z = z
end
end
x_size = 1 + max_x - min_x
y_size = 1 + max_y - min_y
z_size = 1 + max_z - min_z
LOGGER << "Matrix start at (#{min_x}, #{min_y}, #{min_z})\n"
LOGGER << "Matrix dimension are (#{x_size}, #{y_size}, #{z_size})\n"
# Matrix name
add_1_byte_int(MATRIX_NAME.length, io)
add_string(MATRIX_NAME, io)
# Matrix size
add_4_byte_int(x_size, io)
add_4_byte_int(y_size, io)
add_4_byte_int(z_size, io)
# Matrix position
add_4_byte_int(min_x, io)
add_4_byte_int(min_y, io)
add_4_byte_int(min_z, io)
matrix_init_position = io.pos
# Write empty matrix
(x_size * y_size * z_size).times do
io << [0, 0, 0, 0].pack('C*')
end
blocs.each do |bloc|
# Calculate where thr bloc is in the matrix
bloc_x = bloc.x - min_x
bloc_y = bloc.y - min_y
bloc_z = bloc.z - min_z
bloc_position_in_matrix = 4 * (bloc_x + (bloc_y * x_size) + (bloc_z * y_size * x_size))
# Go to the position
io.pos = matrix_init_position + bloc_position_in_matrix
# Color
add_1_byte_int(bloc.r, io)
add_1_byte_int(bloc.g, io)
add_1_byte_int(bloc.b, io)
# Set visible
add_1_byte_int(1, io)
end
end
private
# @param [IO] io
# @return [void]
def add_header(io)
# Version
add_1_byte_int(1, io)
add_1_byte_int(1, io)
add_1_byte_int(0, io)
add_1_byte_int(0, io)
# Color Format
add_4_byte_int(0, io)
# Z-Axis Orientation
add_4_byte_int(1, io)
# Compression
add_4_byte_int(0, io)
# Visibility-Mask encoded
add_4_byte_int(0, io)
# Matrix count
add_4_byte_int(1, io)
end
# @param [Integer] content
# @param [String] io
# @return [void]
def add_4_byte_int(content, io)
io << [content].pack('L*')
end
# @param [Integer] content
# @param [String] io
# @return [void]
def add_1_byte_int(content, io)
io << [content].pack('C*')
end
# @param [String] content
# @param [String] io
# @return [void]
def add_string(content, io)
io << content
end
end
Then to use it:
main.rb
File.open('output.qb', 'wb') do |file|
# create your content
blocs = …
qubicle = Qubicle.new
qubicle.write(blocs, file)
end