Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Exports from MagicaVoxel VOX to OBJ. Can preserve all edges for easy editing in a program like Blender.
"""
AUTOMATIC drag and drop support for windows (NO PROMPT!)
1. Copy script to directory you want your files copied to.
2. Select the files you want to convert.
3. Drag & drop onto this script to convert .vox to .obj!
Files will be exported to directory of this script.
automatic mod by awesomedata
This script is designed to export a mass amount of MagicaVoxel .vox files
to .obj. Unlike Magica's internal exporter, this exporter preserves the
voxel vertices for easy manipulating in a 3d modeling program like Blender.
Various meshing algorithms are included (or to be included). MagicaVoxel
uses monotone triangulation (I think). The algorithms that will (or do)
appear in this script will use methods to potentially reduce rendering
artifacts that could be introduced by triangulation of this nature.
I may also include some features like light map generation for easy
importing into Unreal Engine, etc.
Notes:
* There may be a few floating point equality comparisons. They seem to
work but it scares me a little.
* TODO: use constants instead of magic numbers (as defined in AAQuad),
(i.e., ..., 2 -> AAQuad.TOP, ...)
* A lot of assertions should probably be exceptions since they are
error checking user input (this sounds really bad now that I've put
it on paper...). So don't run in optimized mode (who does that
anyways?).
* I am considering adding FBX support.
"""
import math
class AAQuad:
""" A solid colored axis aligned quad. """
normals = [
(-1, 0, 0), # left = 0
(1, 0, 0), # right = 1
(0, 0, 1), # top = 2
(0, 0, -1), # bottom = 3
(0, -1, 0), # front = 4
(0, 1, 0) # back = 5
]
LEFT = 0
RIGHT = 1
TOP = 2
BOTTOM = 3
FRONT = 4
BACK = 5
def __init__(self, verts, uv=None, normal=None):
assert len(verts) == 4, "face must be a quad"
self.vertices = verts
self.uv = uv
self.normal = normal
def __str__(self):
s = []
for i in self.vertices:
s.append( str(i) + '/' + str(self.uv) + '/' + str(self.normal))
return 'f ' + ' '.join(s)
def center(self):
return (
sum(i[0] for i in self.vertices)/4,
sum(i[1] for i in self.vertices)/4,
sum(i[2] for i in self.vertices)/4
)
def bucketHash(faces, origin, maximum, bucket=16):
extents = (
math.ceil((maximum[0] - origin[0])/bucket),
math.ceil((maximum[1] - origin[1])/bucket),
math.ceil((maximum[2] - origin[2])/bucket)
)
buckets = {}
for f in faces:
c = f.center()
# TODO
def optimizedGreedyMesh(faces):
# TODO
edges = adjacencyGraphEdges(faces)
groups = contiguousFaces(faces, edges)
return faces
def adjacencyGraphEdges(faces):
""" Get the list of edges representing adjacent faces. """
# a list of edges, where edges are tuple(face_a, face_b)
edges = []
# build the list of edges in the graph
for root in faces:
for face in faces:
if face is root:
continue
if facesAreAdjacent(root, face):
# the other edge will happen somewhere else in the iteration
# (i.e., the relation isAdjacent is symmetric)
edges.append((root, face))
return edges
def contiguousFaces(faces, adjacencyGraphEdges):
""" Get the list of connected components from a list of graph edges.
The list will contain lists containing the edges within the components.
"""
groups = []
visited = dict((f, False) for f in faces)
for face in faces:
# if the face hasn't been visited, it is not in any found components
if not visited[face]:
g = []
_visitGraphNodes(face, adjacencyGraphEdges, visited, g)
# there is only a new component if face has not been visited yet
groups.append(g)
return groups
def _visitGraphNodes(node, edges, visited, component):
""" Recursive routine used in findGraphComponents """
# visit every component connected to this one
for edge in edges:
# for all x in nodes, (node, x) and (x, node) should be in edges!
# therefore we don't have to check for "edge[1] is node"
if edge[0] is node and not visited[edge[1]]:
assert edge[1] is not node, "(node, node) should not be in edges"
# mark the other node as visited
visited[edge[1]] = True
component.append(edge[1])
# visit all of that nodes connected nodes
_visitGraphNodes(edge[1], edges, visited, component)
def facesAreAdjacent(a, b):
""" Adjacent is defined as same normal, uv, and a shared edge.
This isn't entirely intuitive (i.e., corner faces are not adjacent)
but this definition fits the problem domain.
Only works on AAQuads.
"""
# note: None is == None, this shouldn't matter
if a.uv != b.uv:
return False
if a.normal != b.normal:
return False
# to be adjacent, two faces must share an edge
# use == and not identity in case edge split was used
shared = 0
for vert_a in a.vertices:
for vert_b in b.vertices:
if vert_a == vert_b:
shared += 1
# hooray we have found a shared edge (or a degenerate case...)
if shared == 2:
return True
return False
class GeoFace:
""" An arbitrary geometry face
This should only be used for arbitrary models, not ones we can
reasonably assume are axis aligned.
"""
def __init__(self, verts, uvs=None, normals=None):
self.vertices = verts
assert len(verts) in (3, 4), "only quads and tris are supported"
self.normals = normals
self.uvs = uvs
def toAAQuad(self, skipAssert=False):
q = AAQuad(self.vertices)
if self.normals is not None and len(self.normals) > 0:
if not skipAssert:
for i in self.normals:
assert self.normals[0] == i, \
"face must be axis aligned (orthogonal normals)"
q.normal = self.normals[0]
if self.uvs is not None and len(self.uvs) > 0:
if not skipAssert:
for i in self.uvs:
assert self.uvs[0] == i, \
"face must be axis aligned (orthogonal)"
q.uv = self.uvs[0]
return q
class VoxelStruct:
""" Describes a voxel object
"""
def __init__(self):
# a dict is probably the best way to go about this
# (as a trade off between performance and code complexity)
# see _index for the indexing method
self.voxels = {}
self.colorIndices = set()
def fromList(self, voxels):
self.voxels = {}
for voxel in voxels:
self.setVoxel(voxel)
self.colorIndices.add(voxel.colorIndex)
def setVoxel(self, voxel):
self.voxels[voxel.z*(256**2) + voxel.y * 256 + voxel.x] = voxel
def getVoxel(self, x, y, z):
return self.voxels.get(z*(256**2) + y * 256 + x, None)
def _index(self, x, y, z):
return z*(256**2) + y * 256 + x
def getBounds(self):
origin = (float("inf"), float("inf"), float("inf"))
maximum = (float("-inf"), float("-inf"), float("-inf"))
for key, voxel in self.voxels.items():
origin = (
min(origin[0], voxel.x),
min(origin[1], voxel.y),
min(origin[2], voxel.z)
)
maximum = (
max(maximum[0], voxel.x),
max(maximum[1], voxel.y),
max(maximum[2], voxel.z)
)
return origin, maximum
def zeroOrigin(self):
""" Translate the model so that it's origin is at 0, 0, 0 """
origin, maximum = self.getBounds()
result = {}
xOff, yOff, zOff = origin
for key, voxel in self.voxels.iteritems():
result[self._index(voxel.x-xOff, voxel.y-yOff, voxel.z-zOff)] = \
Voxel(voxel.x-xOff, voxel.y-yOff, voxel.z-zOff,
voxel.colorIndex)
self.voxels = result
return (0, 0, 0), (maximum[0] - xOff,
maximum[1] - yOff,
maximum[2] - zOff)
def toQuads(self):
""" --> a list of AAQuads """
faces = []
for key, voxel in self.voxels.items():
self._getObjFaces(voxel, faces)
return faces
def _getObjFaces(self, voxel, outFaces):
if voxel.colorIndex == 0:
# do nothing if this is an empty voxel
# n.b., I do not know if this ever can happen.
return []
sides = self._objExposed(voxel)
if sides[0]:
f = self._getLeftSide(voxel)
self._getObjFacesSupport(0, voxel.colorIndex, f, outFaces)
if sides[1]:
f = self._getRightSide(voxel)
self._getObjFacesSupport(1, voxel.colorIndex, f, outFaces)
if sides[2]:
f = self._getTopSide(voxel)
self._getObjFacesSupport(2, voxel.colorIndex, f, outFaces)
if sides[3]:
f = self._getBottomSide(voxel)
self._getObjFacesSupport(3, voxel.colorIndex, f, outFaces)
if sides[4]:
f = self._getFrontSide(voxel)
self._getObjFacesSupport(4, voxel.colorIndex, f, outFaces)
if sides[5]:
f = self._getBackSide(voxel)
self._getObjFacesSupport(5, voxel.colorIndex, f, outFaces)
return
n = AAQuad.normals[i]
# note: texcoords are based on MagicaVoxel's texturing scheme!
# meaning a color index of 0 translates to pixel[255]
# and color index [1:256] -> pixel[0:255]
u = ((voxel.colorIndex - 1)/256 + 1/512, 0.5)
outFaces.append(
# this is most definitely not "fun"
AAQuad(f, u, n)
)
def _getObjFacesSupport(self, side, color, faces, outFaces):
n = AAQuad.normals[side]
# note: texcoords are based on MagicaVoxel's texturing scheme!
# meaning a color index of 0 translates to pixel[255]
# and color index [1:256] -> pixel[0:255]
u = ((color - 1)/256 + 1/512, 0.5)
outFaces.append(
# fact: the parameters were coincidentally "f, u, n" at one point!
AAQuad(faces, u, n)
)
# MagicaVoxel does -.5 to +.5 for each cube, we'll do 0.0 to 1.0 ;)
def _getLeftSide(self, voxel):
return [
(voxel.x, voxel.y + 1, voxel.z + 1),
(voxel.x, voxel.y + 1, voxel.z),
(voxel.x, voxel.y, voxel.z),
(voxel.x, voxel.y, voxel.z + 1)
]
def _getRightSide(self, voxel):
return (
(voxel.x + 1, voxel.y, voxel.z + 1),
(voxel.x + 1, voxel.y, voxel.z),
(voxel.x + 1, voxel.y + 1, voxel.z),
(voxel.x + 1, voxel.y + 1, voxel.z + 1)
)
def _getTopSide(self, voxel):
return (
(voxel.x, voxel.y + 1, voxel.z + 1),
(voxel.x, voxel.y, voxel.z + 1),
(voxel.x + 1, voxel.y, voxel.z + 1),
(voxel.x + 1, voxel.y + 1, voxel.z + 1)
)
def _getBottomSide(self, voxel):
return (
(voxel.x, voxel.y, voxel.z),
(voxel.x, voxel.y + 1, voxel.z),
(voxel.x + 1, voxel.y + 1, voxel.z),
(voxel.x + 1, voxel.y, voxel.z)
)
def _getFrontSide(self, voxel):
return (
(voxel.x, voxel.y, voxel.z + 1),
(voxel.x, voxel.y, voxel.z),
(voxel.x + 1, voxel.y, voxel.z),
(voxel.x + 1, voxel.y, voxel.z + 1)
)
def _getBackSide(self, voxel):
return (
(voxel.x + 1, voxel.y + 1, voxel.z + 1),
(voxel.x + 1, voxel.y + 1, voxel.z),
(voxel.x, voxel.y + 1, voxel.z),
(voxel.x, voxel.y + 1, voxel.z + 1)
)
def _objExposed(self, voxel):
""" --> a set of [0, 6) representing which voxel faces are shown
for the meaning of 0-5, see AAQuad.normals
get the sick truth about these voxels' dirty secrets...
"""
# check left 0
side = self.getVoxel(voxel.x - 1, voxel.y, voxel.z)
s0 = side is None or side.colorIndex == 0
# check right 1
side = self.getVoxel(voxel.x + 1, voxel.y, voxel.z)
s1 = side is None or side.colorIndex == 0
# check top 2
side = self.getVoxel(voxel.x, voxel.y, voxel.z + 1)
s2 = side is None or side.colorIndex == 0
# check bottom 3
side = self.getVoxel(voxel.x, voxel.y, voxel.z - 1)
s3 = side is None or side.colorIndex == 0
# check front 4
side = self.getVoxel(voxel.x, voxel.y - 1, voxel.z)
s4 = side is None or side.colorIndex == 0
# check back 5
side = self.getVoxel(voxel.x, voxel.y + 1, voxel.z)
s5 = side is None or side.colorIndex == 0
return s0, s1, s2, s3, s4, s5
class Voxel:
def __init__(self, x, y, z, colorIndex):
self.x = x
self.y = y
self.z = z
self.colorIndex = colorIndex
def genNormals(self, aaQuads, overwrite=False):
# compute CCW normal if it doesn't exist
for face in aaQuads:
if overwrite or face.normal is None:
side_a = (face.vertices[1][0] - face.vertices[0][0],
face.vertices[1][1] - face.vertices[0][1],
face.vertices[1][2] - face.vertices[0][2])
side_b = (face.vertices[-1][0] - face.vertices[0][0],
face.vertices[-1][1] - face.vertices[0][1],
face.vertices[-1][2] - face.vertices[0][2])
# compute the cross product
face.normal = (side_a[1]*side_b[2] - side_a[2]*side_b[1],
side_a[2]*side_b[0] - side_a[0]*side_b[2],
side_a[0]*side_b[1] - side_a[1]*side_b[0])
def importObj(stream):
vertices = []
faces = []
uvs = []
normals = []
for line in stream:
# make sure there's no new line or trailing spaces
l = line.strip().split(' ')
lineType = l[0].strip()
data = l[1:]
if lineType == 'v':
# vertex
v = tuple(map(float, data))
vertices.append(v)
elif lineType == 'vt':
# uv
uvs.append( tuple(map(float, data)) )
elif lineType == 'vn':
# normal
normals.append( tuple(map(float, data)) )
elif lineType == 'f':
# face (assume all verts/uvs/normals have been processed)
faceVerts = []
faceUvs = []
faceNormals = []
for v in data:
result = v.split('/')
print(result)
# recall that everything is 1 indexed...
faceVerts.append(vertices[int(result[0]) - 1])
if len(result) == 1:
continue # there is only a vertex index
if result[1] != '':
# uvs may not be present, ex: 'f vert//normal ...'
faceUvs.append(uvs[int(result[1]) - 1])
if len(result) <= 2:
# don't continue if only vert and uv are present
continue
faceNormals.append(normals[int(result[2]) - 1])
faces.append( GeoFace(faceVerts, faceUvs, faceNormals) )
else:
# there could be material specs, smoothing, or comments... ignore!
pass
return faces
def exportObj(stream, aaQuads):
# gather some of the needed information
faces = aaQuads
# copy the normals from AAQuad (99% of cases will use all directions)
normals = list(AAQuad.normals)
uvs = set()
for f in faces:
if f.uv is not None:
uvs.add(f.uv)
# convert this to a list because we need to get their index later
uvs = list(uvs)
# we will build a list of vertices as we go and then write everything
# in bulk, disadvantage that MANY verts will be duplicated in the OBJ file
fLines = []
vertices = []
indexOffset = 0
for f in faces:
# recall that OBJ files are 1 indexed
n = 1 + normals.index(f.normal) if f.normal is not None else ''
uv = 1 + uvs.index(f.uv) if f.uv is not None else ''
# this used to be a one liner ;)
fLine = ['f']
for i, vert in enumerate(f.vertices):
# for each vertex of this face
v = 1 + indexOffset + f.vertices.index(vert)
fLine.append(str(v) + '/' + str(uv) + '/' + str(n))
vertices.extend(f.vertices)
indexOffset += len(f.vertices)
fLines.append(' '.join(fLine) + '\n')
# write to the file
stream.write('# shivshank\'s .obj optimizer\n')
stream.write('\n')
if len(normals) > 0:
stream.write('# normals\n')
for n in normals:
stream.write('vn ' + ' '.join(list(map(str, n))) + '\n')
stream.write('\n')
if len(uvs) > 0:
stream.write('# texcoords\n')
for i in uvs:
stream.write('vt ' + ' '.join(list(map(str, i))) + '\n')
stream.write('\n')
# output the vertices and faces
stream.write('# verts\n')
for v in vertices:
stream.write('v ' + ' '.join(list(map(str, v))) + '\n')
stream.write('\n')
stream.write('# faces\n')
for i in fLines:
stream.write(i)
stream.write('\n')
stream.write('\n')
return len(vertices), len(fLines)
def importVox(file):
""" --> a VoxelStruct from this .vox file stream """
# in theory this could elegantly be many functions and classes
# but this is such a simple file format...
# refactor: ? should probably find a better exception type than value error
vox = VoxelStruct()
magic = file.read(4)
if magic != b'VOX ':
print('magic number is', magic)
if userAborts('This does not appear to be a VOX file. Abort?'):
raise ValueError("Invalid magic number")
# the file appears to use little endian consistent with RIFF
version = int.from_bytes(file.read(4), byteorder='little')
if version != 150:
if userAborts('Only version 150 is supported; this file: '
+ str(version) + '. Abort?'):
raise ValueError("Invalid file version")
mainHeader = _readChunkHeader(file)
if mainHeader['id'] != b'MAIN':
print('chunk id:', mainId)
if userAborts('Did not find the main chunk. Abort?'):
raise ValueError("Did not find main VOX chunk. ")
#assert mainHeader['size'] == 0, "main chunk should have size 0"
# we don't need anything from the size or palette header!
# : we can figure out (minimum) bounds later from the voxel data
# : we only need UVs from voxel data; user can export palette elsewhere
nextHeader = _readChunkHeader(file)
while nextHeader['id'] != b'XYZI':
# skip the contents of this header and its children, read the next one
file.read(nextHeader['size'] + nextHeader['childrenSize'])
nextHeader = _readChunkHeader(file)
voxelHeader = nextHeader
assert voxelHeader['id'] == b'XYZI', 'this should be literally impossible'
assert voxelHeader['childrenSize'] == 0, 'why voxel chunk have children?'
seekPos = file.tell()
totalVoxels = int.from_bytes(file.read(4), byteorder='little')
### READ THE VOXELS ###
for i in range(totalVoxels):
# n.b., byte order should be irrelevant since these are all 1 byte
x = int.from_bytes(file.read(1), byteorder='little')
y = int.from_bytes(file.read(1), byteorder='little')
z = int.from_bytes(file.read(1), byteorder='little')
color = int.from_bytes(file.read(1), byteorder='little')
vox.setVoxel(Voxel(x, y, z, color))
# assert that we've read the entire voxel chunk
assert file.tell() - seekPos == voxelHeader['size']
# (there may be more chunks after this but we don't need them!)
#print('\tdone reading voxel data;', totalVoxels , 'voxels read ;D')
return vox
def _readChunkHeader(buffer):
id = buffer.read(4)
if id == b'':
raise ValueError("Unexpected EOF, expected chunk header")
size = int.from_bytes(buffer.read(4), byteorder='little')
childrenSize = int.from_bytes(buffer.read(4), byteorder='little')
return {
'id': id, 'size': size, 'childrenSize': childrenSize
}
def userAborts(msg):
print(msg + ' (y/n)')
u = input()
if u.startswith('n'):
return False
return True
def exportAll():
""" Uses a file to automatically export a bunch of files!
See this function for details on the what the file looks like.
"""
import os, os.path
with open('exporter.txt', mode='r') as file:
# use this as a file "spec"
fromSource = os.path.abspath(file.readline().strip())
toExportDir = os.path.abspath(file.readline().strip())
optimizing = file.readline()
if optimizing.lower() == 'true':
optimizing = True
else:
optimizing = False
print('exporting vox files under', fromSource)
print('\tto directory', toExportDir)
print('\toptimizing?', optimizing)
print()
# export EVERYTHING (.vox) walking the directory structure
for p, dirList, fileList in os.walk(fromSource):
pathDiff = os.path.relpath(p, start=fromSource)
outDir = os.path.join(toExportDir, pathDiff)
# REFACTOR: the loop should be moved to a function
for fileName in fileList:
# only take vox files
if os.path.splitext(fileName)[1] != '.vox':
print('ignored', fileName)
continue
print('exporting', fileName)
# read/import the voxel file
with open(os.path.join(p, fileName), mode='rb') as file:
try:
vox = importVox(file)
except ValueError as exc:
print('aborted', fileName, str(exc))
continue
# mirror the directory structure in the export folder
if not os.path.exists(outDir):
os.makedirs(outDir)
print('\tcreated directory', outDir)
# export a non-optimized version
objName = os.path.splitext(fileName)[0]
rawQuads = vox.toQuads()
with open(os.path.join(outDir, objName + '.obj'), mode='w') as file:
vCount, qCount = exportObj(file, rawQuads)
print('\texported', vCount, 'vertices,', qCount, 'quads')
if optimizing:
# TODO
continue
optiFaces = optimizedGreedyMesh(rawQuads)
bucketHash(optiFaces, *vox.getBounds())
with open(os.path.join(outDir, objName + '.greedy.obj'),
mode='w') as file:
exportObj(file, optiFaces)
def byPrompt():
import os, os.path, sys
from glob import glob
#### set output directory to script file location
# ------------------------------------------------
####
u = os.path.abspath(sys.argv[0]).strip(os.path.basename(sys.argv[0]))
print(u)
#### drag & dropped files
# ---------------------
for i in sys.argv:
if i != sys.argv[0]:
print(i)
#### fully manual prompt ####
# -------------------
# print('Enter an output path:')
# u = input('> ').strip()
while not os.path.exists(u):
print('That path does not exist.')
print('Enter an output path:')
u = input('> ').strip()
outRoot = os.path.abspath(u)
try:
#while True:
#### grab files from prompt (uncomment lines below if needed)
# ----------------------
#print('Enter glob of export files (\'exit\' or blank to quit):')
#u = input('> ').strip()
#if u == 'exit' or u == '':
# break
#u = glob(u)
#### grab drag & dropped files
u = sys.argv
for f in u:
if f != sys.argv[0]:
print('reading VOX file', f)
with open(f, mode='rb') as file:
try:
vox = importVox(file)
except ValueError:
print('\tfile reading aborted')
continue
outFile = os.path.splitext(os.path.basename(f))[0]
outPath = os.path.join(outRoot, outFile+'.obj')
print('exporting VOX to OBJ at path', outPath)
with open(outPath, mode='w') as file:
exportObj(file, vox.toQuads())
except KeyboardInterrupt:
pass
if __name__ == "__main__":
profiling = False
try:
import cProfile
if profiling:
cProfile.run('exportAll()', sort='tottime')
else:
exportAll()
except OSError:
print('No instruction file found, falling back to prompt.')
byPrompt()
"""
This script is designed to export a mass amount of MagicaVoxel .vox files
to .obj. Unlike Magica's internal exporter, this exporter preserves the
voxel vertices for easy manipulating in a 3d modeling program like Blender.
Various meshing algorithms are included (or to be included). MagicaVoxel
uses monotone triangulation (I think). The algorithms that will (or do)
appear in this script will use methods to potentially reduce rendering
artifacts that could be introduced by triangulation of this nature.
I may also include some features like light map generation for easy
importing into Unreal Engine, etc.
Notes:
* There may be a few floating point equality comparisons. They seem to
work but it scares me a little.
* TODO: use constants instead of magic numbers (as defined in AAQuad),
(i.e., ..., 2 -> AAQuad.TOP, ...)
* A lot of assertions should probably be exceptions since they are
error checking user input (this sounds really bad now that I've put
it on paper...). So don't run in optimized mode (who does that
anyways?).
* I am considering adding FBX support.
"""
import math
class AAQuad:
""" A solid colored axis aligned quad. """
normals = [
(-1, 0, 0), # left = 0
(1, 0, 0), # right = 1
(0, 0, 1), # top = 2
(0, 0, -1), # bottom = 3
(0, -1, 0), # front = 4
(0, 1, 0) # back = 5
]
LEFT = 0
RIGHT = 1
TOP = 2
BOTTOM = 3
FRONT = 4
BACK = 5
def __init__(self, verts, uv=None, normal=None):
assert len(verts) == 4, "face must be a quad"
self.vertices = verts
self.uv = uv
self.normal = normal
def __str__(self):
s = []
for i in self.vertices:
s.append( str(i) + '/' + str(self.uv) + '/' + str(self.normal))
return 'f ' + ' '.join(s)
def center(self):
return (
sum(i[0] for i in self.vertices)/4,
sum(i[1] for i in self.vertices)/4,
sum(i[2] for i in self.vertices)/4
)
def bucketHash(faces, origin, maximum, bucket=16):
extents = (
math.ceil((maximum[0] - origin[0])/bucket),
math.ceil((maximum[1] - origin[1])/bucket),
math.ceil((maximum[2] - origin[2])/bucket)
)
buckets = {}
for f in faces:
c = f.center()
# TODO
def optimizedGreedyMesh(faces):
# TODO
edges = adjacencyGraphEdges(faces)
groups = contiguousFaces(faces, edges)
return faces
def adjacencyGraphEdges(faces):
""" Get the list of edges representing adjacent faces. """
# a list of edges, where edges are tuple(face_a, face_b)
edges = []
# build the list of edges in the graph
for root in faces:
for face in faces:
if face is root:
continue
if facesAreAdjacent(root, face):
# the other edge will happen somewhere else in the iteration
# (i.e., the relation isAdjacent is symmetric)
edges.append((root, face))
return edges
def contiguousFaces(faces, adjacencyGraphEdges):
""" Get the list of connected components from a list of graph edges.
The list will contain lists containing the edges within the components.
"""
groups = []
visited = dict((f, False) for f in faces)
for face in faces:
# if the face hasn't been visited, it is not in any found components
if not visited[face]:
g = []
_visitGraphNodes(face, adjacencyGraphEdges, visited, g)
# there is only a new component if face has not been visited yet
groups.append(g)
return groups
def _visitGraphNodes(node, edges, visited, component):
""" Recursive routine used in findGraphComponents """
# visit every component connected to this one
for edge in edges:
# for all x in nodes, (node, x) and (x, node) should be in edges!
# therefore we don't have to check for "edge[1] is node"
if edge[0] is node and not visited[edge[1]]:
assert edge[1] is not node, "(node, node) should not be in edges"
# mark the other node as visited
visited[edge[1]] = True
component.append(edge[1])
# visit all of that nodes connected nodes
_visitGraphNodes(edge[1], edges, visited, component)
def facesAreAdjacent(a, b):
""" Adjacent is defined as same normal, uv, and a shared edge.
This isn't entirely intuitive (i.e., corner faces are not adjacent)
but this definition fits the problem domain.
Only works on AAQuads.
"""
# note: None is == None, this shouldn't matter
if a.uv != b.uv:
return False
if a.normal != b.normal:
return False
# to be adjacent, two faces must share an edge
# use == and not identity in case edge split was used
shared = 0
for vert_a in a.vertices:
for vert_b in b.vertices:
if vert_a == vert_b:
shared += 1
# hooray we have found a shared edge (or a degenerate case...)
if shared == 2:
return True
return False
class GeoFace:
""" An arbitrary geometry face
This should only be used for arbitrary models, not ones we can
reasonably assume are axis aligned.
"""
def __init__(self, verts, uvs=None, normals=None):
self.vertices = verts
assert len(verts) in (3, 4), "only quads and tris are supported"
self.normals = normals
self.uvs = uvs
def toAAQuad(self, skipAssert=False):
q = AAQuad(self.vertices)
if self.normals is not None and len(self.normals) > 0:
if not skipAssert:
for i in self.normals:
assert self.normals[0] == i, \
"face must be axis aligned (orthogonal normals)"
q.normal = self.normals[0]
if self.uvs is not None and len(self.uvs) > 0:
if not skipAssert:
for i in self.uvs:
assert self.uvs[0] == i, \
"face must be axis aligned (orthogonal)"
q.uv = self.uvs[0]
return q
class VoxelStruct:
""" Describes a voxel object
"""
def __init__(self):
# a dict is probably the best way to go about this
# (as a trade off between performance and code complexity)
# see _index for the indexing method
self.voxels = {}
self.colorIndices = set()
def fromList(self, voxels):
self.voxels = {}
for voxel in voxels:
self.setVoxel(voxel)
self.colorIndices.add(voxel.colorIndex)
def setVoxel(self, voxel):
self.voxels[voxel.z*(256**2) + voxel.y * 256 + voxel.x] = voxel
def getVoxel(self, x, y, z):
return self.voxels.get(z*(256**2) + y * 256 + x, None)
def _index(self, x, y, z):
return z*(256**2) + y * 256 + x
def getBounds(self):
origin = (float("inf"), float("inf"), float("inf"))
maximum = (float("-inf"), float("-inf"), float("-inf"))
for key, voxel in self.voxels.items():
origin = (
min(origin[0], voxel.x),
min(origin[1], voxel.y),
min(origin[2], voxel.z)
)
maximum = (
max(maximum[0], voxel.x),
max(maximum[1], voxel.y),
max(maximum[2], voxel.z)
)
return origin, maximum
def zeroOrigin(self):
""" Translate the model so that it's origin is at 0, 0, 0 """
origin, maximum = self.getBounds()
result = {}
xOff, yOff, zOff = origin
for key, voxel in self.voxels.iteritems():
result[self._index(voxel.x-xOff, voxel.y-yOff, voxel.z-zOff)] = \
Voxel(voxel.x-xOff, voxel.y-yOff, voxel.z-zOff,
voxel.colorIndex)
self.voxels = result
return (0, 0, 0), (maximum[0] - xOff,
maximum[1] - yOff,
maximum[2] - zOff)
def toQuads(self):
""" --> a list of AAQuads """
faces = []
for key, voxel in self.voxels.items():
self._getObjFaces(voxel, faces)
return faces
def _getObjFaces(self, voxel, outFaces):
if voxel.colorIndex == 0:
# do nothing if this is an empty voxel
# n.b., I do not know if this ever can happen.
return []
sides = self._objExposed(voxel)
if sides[0]:
f = self._getLeftSide(voxel)
self._getObjFacesSupport(0, voxel.colorIndex, f, outFaces)
if sides[1]:
f = self._getRightSide(voxel)
self._getObjFacesSupport(1, voxel.colorIndex, f, outFaces)
if sides[2]:
f = self._getTopSide(voxel)
self._getObjFacesSupport(2, voxel.colorIndex, f, outFaces)
if sides[3]:
f = self._getBottomSide(voxel)
self._getObjFacesSupport(3, voxel.colorIndex, f, outFaces)
if sides[4]:
f = self._getFrontSide(voxel)
self._getObjFacesSupport(4, voxel.colorIndex, f, outFaces)
if sides[5]:
f = self._getBackSide(voxel)
self._getObjFacesSupport(5, voxel.colorIndex, f, outFaces)
return
n = AAQuad.normals[i]
# note: texcoords are based on MagicaVoxel's texturing scheme!
# meaning a color index of 0 translates to pixel[255]
# and color index [1:256] -> pixel[0:255]
u = ((voxel.colorIndex - 1)/256 + 1/512, 0.5)
outFaces.append(
# this is most definitely not "fun"
AAQuad(f, u, n)
)
def _getObjFacesSupport(self, side, color, faces, outFaces):
n = AAQuad.normals[side]
# note: texcoords are based on MagicaVoxel's texturing scheme!
# meaning a color index of 0 translates to pixel[255]
# and color index [1:256] -> pixel[0:255]
u = ((color - 1)/256 + 1/512, 0.5)
outFaces.append(
# fact: the parameters were coincidentally "f, u, n" at one point!
AAQuad(faces, u, n)
)
# MagicaVoxel does -.5 to +.5 for each cube, we'll do 0.0 to 1.0 ;)
def _getLeftSide(self, voxel):
return [
(voxel.x, voxel.y + 1, voxel.z + 1),
(voxel.x, voxel.y + 1, voxel.z),
(voxel.x, voxel.y, voxel.z),
(voxel.x, voxel.y, voxel.z + 1)
]
def _getRightSide(self, voxel):
return (
(voxel.x + 1, voxel.y, voxel.z + 1),
(voxel.x + 1, voxel.y, voxel.z),
(voxel.x + 1, voxel.y + 1, voxel.z),
(voxel.x + 1, voxel.y + 1, voxel.z + 1)
)
def _getTopSide(self, voxel):
return (
(voxel.x, voxel.y + 1, voxel.z + 1),
(voxel.x, voxel.y, voxel.z + 1),
(voxel.x + 1, voxel.y, voxel.z + 1),
(voxel.x + 1, voxel.y + 1, voxel.z + 1)
)
def _getBottomSide(self, voxel):
return (
(voxel.x, voxel.y, voxel.z),
(voxel.x, voxel.y + 1, voxel.z),
(voxel.x + 1, voxel.y + 1, voxel.z),
(voxel.x + 1, voxel.y, voxel.z)
)
def _getFrontSide(self, voxel):
return (
(voxel.x, voxel.y, voxel.z + 1),
(voxel.x, voxel.y, voxel.z),
(voxel.x + 1, voxel.y, voxel.z),
(voxel.x + 1, voxel.y, voxel.z + 1)
)
def _getBackSide(self, voxel):
return (
(voxel.x + 1, voxel.y + 1, voxel.z + 1),
(voxel.x + 1, voxel.y + 1, voxel.z),
(voxel.x, voxel.y + 1, voxel.z),
(voxel.x, voxel.y + 1, voxel.z + 1)
)
def _objExposed(self, voxel):
""" --> a set of [0, 6) representing which voxel faces are shown
for the meaning of 0-5, see AAQuad.normals
get the sick truth about these voxels' dirty secrets...
"""
# check left 0
side = self.getVoxel(voxel.x - 1, voxel.y, voxel.z)
s0 = side is None or side.colorIndex == 0
# check right 1
side = self.getVoxel(voxel.x + 1, voxel.y, voxel.z)
s1 = side is None or side.colorIndex == 0
# check top 2
side = self.getVoxel(voxel.x, voxel.y, voxel.z + 1)
s2 = side is None or side.colorIndex == 0
# check bottom 3
side = self.getVoxel(voxel.x, voxel.y, voxel.z - 1)
s3 = side is None or side.colorIndex == 0
# check front 4
side = self.getVoxel(voxel.x, voxel.y - 1, voxel.z)
s4 = side is None or side.colorIndex == 0
# check back 5
side = self.getVoxel(voxel.x, voxel.y + 1, voxel.z)
s5 = side is None or side.colorIndex == 0
return s0, s1, s2, s3, s4, s5
class Voxel:
def __init__(self, x, y, z, colorIndex):
self.x = x
self.y = y
self.z = z
self.colorIndex = colorIndex
def genNormals(self, aaQuads, overwrite=False):
# compute CCW normal if it doesn't exist
for face in aaQuads:
if overwrite or face.normal is None:
side_a = (face.vertices[1][0] - face.vertices[0][0],
face.vertices[1][1] - face.vertices[0][1],
face.vertices[1][2] - face.vertices[0][2])
side_b = (face.vertices[-1][0] - face.vertices[0][0],
face.vertices[-1][1] - face.vertices[0][1],
face.vertices[-1][2] - face.vertices[0][2])
# compute the cross product
face.normal = (side_a[1]*side_b[2] - side_a[2]*side_b[1],
side_a[2]*side_b[0] - side_a[0]*side_b[2],
side_a[0]*side_b[1] - side_a[1]*side_b[0])
def importObj(stream):
vertices = []
faces = []
uvs = []
normals = []
for line in stream:
# make sure there's no new line or trailing spaces
l = line.strip().split(' ')
lineType = l[0].strip()
data = l[1:]
if lineType == 'v':
# vertex
v = tuple(map(float, data))
vertices.append(v)
elif lineType == 'vt':
# uv
uvs.append( tuple(map(float, data)) )
elif lineType == 'vn':
# normal
normals.append( tuple(map(float, data)) )
elif lineType == 'f':
# face (assume all verts/uvs/normals have been processed)
faceVerts = []
faceUvs = []
faceNormals = []
for v in data:
result = v.split('/')
print(result)
# recall that everything is 1 indexed...
faceVerts.append(vertices[int(result[0]) - 1])
if len(result) == 1:
continue # there is only a vertex index
if result[1] != '':
# uvs may not be present, ex: 'f vert//normal ...'
faceUvs.append(uvs[int(result[1]) - 1])
if len(result) <= 2:
# don't continue if only vert and uv are present
continue
faceNormals.append(normals[int(result[2]) - 1])
faces.append( GeoFace(faceVerts, faceUvs, faceNormals) )
else:
# there could be material specs, smoothing, or comments... ignore!
pass
return faces
def exportObj(stream, aaQuads):
# gather some of the needed information
faces = aaQuads
# copy the normals from AAQuad (99% of cases will use all directions)
normals = list(AAQuad.normals)
uvs = set()
for f in faces:
if f.uv is not None:
uvs.add(f.uv)
# convert this to a list because we need to get their index later
uvs = list(uvs)
# we will build a list of vertices as we go and then write everything
# in bulk, disadvantage that MANY verts will be duplicated in the OBJ file
fLines = []
vertices = []
indexOffset = 0
for f in faces:
# recall that OBJ files are 1 indexed
n = 1 + normals.index(f.normal) if f.normal is not None else ''
uv = 1 + uvs.index(f.uv) if f.uv is not None else ''
# this used to be a one liner ;)
fLine = ['f']
for i, vert in enumerate(f.vertices):
# for each vertex of this face
v = 1 + indexOffset + f.vertices.index(vert)
fLine.append(str(v) + '/' + str(uv) + '/' + str(n))
vertices.extend(f.vertices)
indexOffset += len(f.vertices)
fLines.append(' '.join(fLine) + '\n')
# write to the file
stream.write('# shivshank\'s .obj optimizer\n')
stream.write('\n')
if len(normals) > 0:
stream.write('# normals\n')
for n in normals:
stream.write('vn ' + ' '.join(list(map(str, n))) + '\n')
stream.write('\n')
if len(uvs) > 0:
stream.write('# texcoords\n')
for i in uvs:
stream.write('vt ' + ' '.join(list(map(str, i))) + '\n')
stream.write('\n')
# output the vertices and faces
stream.write('# verts\n')
for v in vertices:
stream.write('v ' + ' '.join(list(map(str, v))) + '\n')
stream.write('\n')
stream.write('# faces\n')
for i in fLines:
stream.write(i)
stream.write('\n')
stream.write('\n')
return len(vertices), len(fLines)
def importVox(file):
""" --> a VoxelStruct from this .vox file stream """
# in theory this could elegantly be many functions and classes
# but this is such a simple file format...
# refactor: ? should probably find a better exception type than value error
vox = VoxelStruct()
magic = file.read(4)
if magic != b'VOX ':
print('magic number is', magic)
if userAborts('This does not appear to be a VOX file. Abort?'):
raise ValueError("Invalid magic number")
# the file appears to use little endian consistent with RIFF
version = int.from_bytes(file.read(4), byteorder='little')
if version != 150:
if userAborts('Only version 150 is supported; this file: '
+ str(version) + '. Abort?'):
raise ValueError("Invalid file version")
mainHeader = _readChunkHeader(file)
if mainHeader['id'] != b'MAIN':
print('chunk id:', mainId)
if userAborts('Did not find the main chunk. Abort?'):
raise ValueError("Did not find main VOX chunk. ")
#assert mainHeader['size'] == 0, "main chunk should have size 0"
# we don't need anything from the size or palette header!
# : we can figure out (minimum) bounds later from the voxel data
# : we only need UVs from voxel data; user can export palette elsewhere
nextHeader = _readChunkHeader(file)
while nextHeader['id'] != b'XYZI':
# skip the contents of this header and its children, read the next one
file.read(nextHeader['size'] + nextHeader['childrenSize'])
nextHeader = _readChunkHeader(file)
voxelHeader = nextHeader
assert voxelHeader['id'] == b'XYZI', 'this should be literally impossible'
assert voxelHeader['childrenSize'] == 0, 'why voxel chunk have children?'
seekPos = file.tell()
totalVoxels = int.from_bytes(file.read(4), byteorder='little')
### READ THE VOXELS ###
for i in range(totalVoxels):
# n.b., byte order should be irrelevant since these are all 1 byte
x = int.from_bytes(file.read(1), byteorder='little')
y = int.from_bytes(file.read(1), byteorder='little')
z = int.from_bytes(file.read(1), byteorder='little')
color = int.from_bytes(file.read(1), byteorder='little')
vox.setVoxel(Voxel(x, y, z, color))
# assert that we've read the entire voxel chunk
assert file.tell() - seekPos == voxelHeader['size']
# (there may be more chunks after this but we don't need them!)
#print('\tdone reading voxel data;', totalVoxels , 'voxels read ;D')
return vox
def _readChunkHeader(buffer):
id = buffer.read(4)
if id == b'':
raise ValueError("Unexpected EOF, expected chunk header")
size = int.from_bytes(buffer.read(4), byteorder='little')
childrenSize = int.from_bytes(buffer.read(4), byteorder='little')
return {
'id': id, 'size': size, 'childrenSize': childrenSize
}
def userAborts(msg):
print(msg + ' (y/n)')
u = input()
if u.startswith('n'):
return False
return True
def exportAll():
""" Uses a file to automatically export a bunch of files!
See this function for details on the what the file looks like.
"""
import os, os.path
with open('exporter.txt', mode='r') as file:
# use this as a file "spec"
fromSource = os.path.abspath(file.readline().strip())
toExportDir = os.path.abspath(file.readline().strip())
optimizing = file.readline()
if optimizing.lower() == 'true':
optimizing = True
else:
optimizing = False
print('exporting vox files under', fromSource)
print('\tto directory', toExportDir)
print('\toptimizing?', optimizing)
print()
# export EVERYTHING (.vox) walking the directory structure
for p, dirList, fileList in os.walk(fromSource):
pathDiff = os.path.relpath(p, start=fromSource)
outDir = os.path.join(toExportDir, pathDiff)
# REFACTOR: the loop should be moved to a function
for fileName in fileList:
# only take vox files
if os.path.splitext(fileName)[1] != '.vox':
print('ignored', fileName)
continue
print('exporting', fileName)
# read/import the voxel file
with open(os.path.join(p, fileName), mode='rb') as file:
try:
vox = importVox(file)
except ValueError as exc:
print('aborted', fileName, str(exc))
continue
# mirror the directory structure in the export folder
if not os.path.exists(outDir):
os.makedirs(outDir)
print('\tcreated directory', outDir)
# export a non-optimized version
objName = os.path.splitext(fileName)[0]
rawQuads = vox.toQuads()
with open(os.path.join(outDir, objName + '.obj'), mode='w') as file:
vCount, qCount = exportObj(file, rawQuads)
print('\texported', vCount, 'vertices,', qCount, 'quads')
if optimizing:
# TODO
continue
optiFaces = optimizedGreedyMesh(rawQuads)
bucketHash(optiFaces, *vox.getBounds())
with open(os.path.join(outDir, objName + '.greedy.obj'),
mode='w') as file:
exportObj(file, optiFaces)
def byPrompt():
import os, os.path
from glob import glob
print('Enter an output path:')
u = input('> ').strip()
while not os.path.exists(u):
print('That path does not exist.')
print('Enter an output path:')
u = input('> ').strip()
outRoot = os.path.abspath(u)
print('Are we optimizing? (y/n)')
u = input('> ').strip()
# this could be a one liner but I think it's easier to read this way
if u.startswith('y'):
optimizing = True
else:
optimizing = False
try:
while True:
print('Enter glob of export files (\'exit\' or blank to quit):')
u = input('> ').strip()
if u == 'exit' or u == '':
break
u = glob(u)
for f in u:
print('reading VOX file', f)
with open(f, mode='rb') as file:
try:
vox = importVox(file)
except ValueError:
print('\tfile reading aborted')
continue
outFile = os.path.splitext(os.path.basename(f))[0]
outPath = os.path.join(outRoot, outFile)
print('exporting VOX to OBJ at path', outPath)
with open(outPath, mode='w') as file:
exportObj(file, vox.toQuads())
if optimizing:
# TODO
pass
except KeyboardInterrupt:
pass
if __name__ == "__main__":
profiling = False
try:
import cProfile
if profiling:
cProfile.run('exportAll()', sort='tottime')
else:
exportAll()
except OSError:
print('No instruction file found, falling back to prompt.')
byPrompt()
"""
SEMI-AUTOMATIC drag and drop support for windows
1. Copy script to directory you want your files copied to.
2. Select the files you want to convert.
3. Drag & drop onto this script.
4. Prompt will appear -- Press "enter" to convert .vox to .obj! (or abort with "y")
Files will be exported to directory of this script.
semi-automatic mod by awesomedata
This script is designed to export a mass amount of MagicaVoxel .vox files
to .obj. Unlike Magica's internal exporter, this exporter preserves the
voxel vertices for easy manipulating in a 3d modeling program like Blender.
Various meshing algorithms are included (or to be included). MagicaVoxel
uses monotone triangulation (I think). The algorithms that will (or do)
appear in this script will use methods to potentially reduce rendering
artifacts that could be introduced by triangulation of this nature.
I may also include some features like light map generation for easy
importing into Unreal Engine, etc.
Notes:
* There may be a few floating point equality comparisons. They seem to
work but it scares me a little.
* TODO: use constants instead of magic numbers (as defined in AAQuad),
(i.e., ..., 2 -> AAQuad.TOP, ...)
* A lot of assertions should probably be exceptions since they are
error checking user input (this sounds really bad now that I've put
it on paper...). So don't run in optimized mode (who does that
anyways?).
* I am considering adding FBX support.
"""
import math
class AAQuad:
""" A solid colored axis aligned quad. """
normals = [
(-1, 0, 0), # left = 0
(1, 0, 0), # right = 1
(0, 0, 1), # top = 2
(0, 0, -1), # bottom = 3
(0, -1, 0), # front = 4
(0, 1, 0) # back = 5
]
LEFT = 0
RIGHT = 1
TOP = 2
BOTTOM = 3
FRONT = 4
BACK = 5
def __init__(self, verts, uv=None, normal=None):
assert len(verts) == 4, "face must be a quad"
self.vertices = verts
self.uv = uv
self.normal = normal
def __str__(self):
s = []
for i in self.vertices:
s.append( str(i) + '/' + str(self.uv) + '/' + str(self.normal))
return 'f ' + ' '.join(s)
def center(self):
return (
sum(i[0] for i in self.vertices)/4,
sum(i[1] for i in self.vertices)/4,
sum(i[2] for i in self.vertices)/4
)
def bucketHash(faces, origin, maximum, bucket=16):
extents = (
math.ceil((maximum[0] - origin[0])/bucket),
math.ceil((maximum[1] - origin[1])/bucket),
math.ceil((maximum[2] - origin[2])/bucket)
)
buckets = {}
for f in faces:
c = f.center()
# TODO
def optimizedGreedyMesh(faces):
# TODO
edges = adjacencyGraphEdges(faces)
groups = contiguousFaces(faces, edges)
return faces
def adjacencyGraphEdges(faces):
""" Get the list of edges representing adjacent faces. """
# a list of edges, where edges are tuple(face_a, face_b)
edges = []
# build the list of edges in the graph
for root in faces:
for face in faces:
if face is root:
continue
if facesAreAdjacent(root, face):
# the other edge will happen somewhere else in the iteration
# (i.e., the relation isAdjacent is symmetric)
edges.append((root, face))
return edges
def contiguousFaces(faces, adjacencyGraphEdges):
""" Get the list of connected components from a list of graph edges.
The list will contain lists containing the edges within the components.
"""
groups = []
visited = dict((f, False) for f in faces)
for face in faces:
# if the face hasn't been visited, it is not in any found components
if not visited[face]:
g = []
_visitGraphNodes(face, adjacencyGraphEdges, visited, g)
# there is only a new component if face has not been visited yet
groups.append(g)
return groups
def _visitGraphNodes(node, edges, visited, component):
""" Recursive routine used in findGraphComponents """
# visit every component connected to this one
for edge in edges:
# for all x in nodes, (node, x) and (x, node) should be in edges!
# therefore we don't have to check for "edge[1] is node"
if edge[0] is node and not visited[edge[1]]:
assert edge[1] is not node, "(node, node) should not be in edges"
# mark the other node as visited
visited[edge[1]] = True
component.append(edge[1])
# visit all of that nodes connected nodes
_visitGraphNodes(edge[1], edges, visited, component)
def facesAreAdjacent(a, b):
""" Adjacent is defined as same normal, uv, and a shared edge.
This isn't entirely intuitive (i.e., corner faces are not adjacent)
but this definition fits the problem domain.
Only works on AAQuads.
"""
# note: None is == None, this shouldn't matter
if a.uv != b.uv:
return False
if a.normal != b.normal:
return False
# to be adjacent, two faces must share an edge
# use == and not identity in case edge split was used
shared = 0
for vert_a in a.vertices:
for vert_b in b.vertices:
if vert_a == vert_b:
shared += 1
# hooray we have found a shared edge (or a degenerate case...)
if shared == 2:
return True
return False
class GeoFace:
""" An arbitrary geometry face
This should only be used for arbitrary models, not ones we can
reasonably assume are axis aligned.
"""
def __init__(self, verts, uvs=None, normals=None):
self.vertices = verts
assert len(verts) in (3, 4), "only quads and tris are supported"
self.normals = normals
self.uvs = uvs
def toAAQuad(self, skipAssert=False):
q = AAQuad(self.vertices)
if self.normals is not None and len(self.normals) > 0:
if not skipAssert:
for i in self.normals:
assert self.normals[0] == i, \
"face must be axis aligned (orthogonal normals)"
q.normal = self.normals[0]
if self.uvs is not None and len(self.uvs) > 0:
if not skipAssert:
for i in self.uvs:
assert self.uvs[0] == i, \
"face must be axis aligned (orthogonal)"
q.uv = self.uvs[0]
return q
class VoxelStruct:
""" Describes a voxel object
"""
def __init__(self):
# a dict is probably the best way to go about this
# (as a trade off between performance and code complexity)
# see _index for the indexing method
self.voxels = {}
self.colorIndices = set()
def fromList(self, voxels):
self.voxels = {}
for voxel in voxels:
self.setVoxel(voxel)
self.colorIndices.add(voxel.colorIndex)
def setVoxel(self, voxel):
self.voxels[voxel.z*(256**2) + voxel.y * 256 + voxel.x] = voxel
def getVoxel(self, x, y, z):
return self.voxels.get(z*(256**2) + y * 256 + x, None)
def _index(self, x, y, z):
return z*(256**2) + y * 256 + x
def getBounds(self):
origin = (float("inf"), float("inf"), float("inf"))
maximum = (float("-inf"), float("-inf"), float("-inf"))
for key, voxel in self.voxels.items():
origin = (
min(origin[0], voxel.x),
min(origin[1], voxel.y),
min(origin[2], voxel.z)
)
maximum = (
max(maximum[0], voxel.x),
max(maximum[1], voxel.y),
max(maximum[2], voxel.z)
)
return origin, maximum
def zeroOrigin(self):
""" Translate the model so that it's origin is at 0, 0, 0 """
origin, maximum = self.getBounds()
result = {}
xOff, yOff, zOff = origin
for key, voxel in self.voxels.iteritems():
result[self._index(voxel.x-xOff, voxel.y-yOff, voxel.z-zOff)] = \
Voxel(voxel.x-xOff, voxel.y-yOff, voxel.z-zOff,
voxel.colorIndex)
self.voxels = result
return (0, 0, 0), (maximum[0] - xOff,
maximum[1] - yOff,
maximum[2] - zOff)
def toQuads(self):
""" --> a list of AAQuads """
faces = []
for key, voxel in self.voxels.items():
self._getObjFaces(voxel, faces)
return faces
def _getObjFaces(self, voxel, outFaces):
if voxel.colorIndex == 0:
# do nothing if this is an empty voxel
# n.b., I do not know if this ever can happen.
return []
sides = self._objExposed(voxel)
if sides[0]:
f = self._getLeftSide(voxel)
self._getObjFacesSupport(0, voxel.colorIndex, f, outFaces)
if sides[1]:
f = self._getRightSide(voxel)
self._getObjFacesSupport(1, voxel.colorIndex, f, outFaces)
if sides[2]:
f = self._getTopSide(voxel)
self._getObjFacesSupport(2, voxel.colorIndex, f, outFaces)
if sides[3]:
f = self._getBottomSide(voxel)
self._getObjFacesSupport(3, voxel.colorIndex, f, outFaces)
if sides[4]:
f = self._getFrontSide(voxel)
self._getObjFacesSupport(4, voxel.colorIndex, f, outFaces)
if sides[5]:
f = self._getBackSide(voxel)
self._getObjFacesSupport(5, voxel.colorIndex, f, outFaces)
return
n = AAQuad.normals[i]
# note: texcoords are based on MagicaVoxel's texturing scheme!
# meaning a color index of 0 translates to pixel[255]
# and color index [1:256] -> pixel[0:255]
u = ((voxel.colorIndex - 1)/256 + 1/512, 0.5)
outFaces.append(
# this is most definitely not "fun"
AAQuad(f, u, n)
)
def _getObjFacesSupport(self, side, color, faces, outFaces):
n = AAQuad.normals[side]
# note: texcoords are based on MagicaVoxel's texturing scheme!
# meaning a color index of 0 translates to pixel[255]
# and color index [1:256] -> pixel[0:255]
u = ((color - 1)/256 + 1/512, 0.5)
outFaces.append(
# fact: the parameters were coincidentally "f, u, n" at one point!
AAQuad(faces, u, n)
)
# MagicaVoxel does -.5 to +.5 for each cube, we'll do 0.0 to 1.0 ;)
def _getLeftSide(self, voxel):
return [
(voxel.x, voxel.y + 1, voxel.z + 1),
(voxel.x, voxel.y + 1, voxel.z),
(voxel.x, voxel.y, voxel.z),
(voxel.x, voxel.y, voxel.z + 1)
]
def _getRightSide(self, voxel):
return (
(voxel.x + 1, voxel.y, voxel.z + 1),
(voxel.x + 1, voxel.y, voxel.z),
(voxel.x + 1, voxel.y + 1, voxel.z),
(voxel.x + 1, voxel.y + 1, voxel.z + 1)
)
def _getTopSide(self, voxel):
return (
(voxel.x, voxel.y + 1, voxel.z + 1),
(voxel.x, voxel.y, voxel.z + 1),
(voxel.x + 1, voxel.y, voxel.z + 1),
(voxel.x + 1, voxel.y + 1, voxel.z + 1)
)
def _getBottomSide(self, voxel):
return (
(voxel.x, voxel.y, voxel.z),
(voxel.x, voxel.y + 1, voxel.z),
(voxel.x + 1, voxel.y + 1, voxel.z),
(voxel.x + 1, voxel.y, voxel.z)
)
def _getFrontSide(self, voxel):
return (
(voxel.x, voxel.y, voxel.z + 1),
(voxel.x, voxel.y, voxel.z),
(voxel.x + 1, voxel.y, voxel.z),
(voxel.x + 1, voxel.y, voxel.z + 1)
)
def _getBackSide(self, voxel):
return (
(voxel.x + 1, voxel.y + 1, voxel.z + 1),
(voxel.x + 1, voxel.y + 1, voxel.z),
(voxel.x, voxel.y + 1, voxel.z),
(voxel.x, voxel.y + 1, voxel.z + 1)
)
def _objExposed(self, voxel):
""" --> a set of [0, 6) representing which voxel faces are shown
for the meaning of 0-5, see AAQuad.normals
get the sick truth about these voxels' dirty secrets...
"""
# check left 0
side = self.getVoxel(voxel.x - 1, voxel.y, voxel.z)
s0 = side is None or side.colorIndex == 0
# check right 1
side = self.getVoxel(voxel.x + 1, voxel.y, voxel.z)
s1 = side is None or side.colorIndex == 0
# check top 2
side = self.getVoxel(voxel.x, voxel.y, voxel.z + 1)
s2 = side is None or side.colorIndex == 0
# check bottom 3
side = self.getVoxel(voxel.x, voxel.y, voxel.z - 1)
s3 = side is None or side.colorIndex == 0
# check front 4
side = self.getVoxel(voxel.x, voxel.y - 1, voxel.z)
s4 = side is None or side.colorIndex == 0
# check back 5
side = self.getVoxel(voxel.x, voxel.y + 1, voxel.z)
s5 = side is None or side.colorIndex == 0
return s0, s1, s2, s3, s4, s5
class Voxel:
def __init__(self, x, y, z, colorIndex):
self.x = x
self.y = y
self.z = z
self.colorIndex = colorIndex
def genNormals(self, aaQuads, overwrite=False):
# compute CCW normal if it doesn't exist
for face in aaQuads:
if overwrite or face.normal is None:
side_a = (face.vertices[1][0] - face.vertices[0][0],
face.vertices[1][1] - face.vertices[0][1],
face.vertices[1][2] - face.vertices[0][2])
side_b = (face.vertices[-1][0] - face.vertices[0][0],
face.vertices[-1][1] - face.vertices[0][1],
face.vertices[-1][2] - face.vertices[0][2])
# compute the cross product
face.normal = (side_a[1]*side_b[2] - side_a[2]*side_b[1],
side_a[2]*side_b[0] - side_a[0]*side_b[2],
side_a[0]*side_b[1] - side_a[1]*side_b[0])
def importObj(stream):
vertices = []
faces = []
uvs = []
normals = []
for line in stream:
# make sure there's no new line or trailing spaces
l = line.strip().split(' ')
lineType = l[0].strip()
data = l[1:]
if lineType == 'v':
# vertex
v = tuple(map(float, data))
vertices.append(v)
elif lineType == 'vt':
# uv
uvs.append( tuple(map(float, data)) )
elif lineType == 'vn':
# normal
normals.append( tuple(map(float, data)) )
elif lineType == 'f':
# face (assume all verts/uvs/normals have been processed)
faceVerts = []
faceUvs = []
faceNormals = []
for v in data:
result = v.split('/')
print(result)
# recall that everything is 1 indexed...
faceVerts.append(vertices[int(result[0]) - 1])
if len(result) == 1:
continue # there is only a vertex index
if result[1] != '':
# uvs may not be present, ex: 'f vert//normal ...'
faceUvs.append(uvs[int(result[1]) - 1])
if len(result) <= 2:
# don't continue if only vert and uv are present
continue
faceNormals.append(normals[int(result[2]) - 1])
faces.append( GeoFace(faceVerts, faceUvs, faceNormals) )
else:
# there could be material specs, smoothing, or comments... ignore!
pass
return faces
def exportObj(stream, aaQuads):
# gather some of the needed information
faces = aaQuads
# copy the normals from AAQuad (99% of cases will use all directions)
normals = list(AAQuad.normals)
uvs = set()
for f in faces:
if f.uv is not None:
uvs.add(f.uv)
# convert this to a list because we need to get their index later
uvs = list(uvs)
# we will build a list of vertices as we go and then write everything
# in bulk, disadvantage that MANY verts will be duplicated in the OBJ file
fLines = []
vertices = []
indexOffset = 0
for f in faces:
# recall that OBJ files are 1 indexed
n = 1 + normals.index(f.normal) if f.normal is not None else ''
uv = 1 + uvs.index(f.uv) if f.uv is not None else ''
# this used to be a one liner ;)
fLine = ['f']
for i, vert in enumerate(f.vertices):
# for each vertex of this face
v = 1 + indexOffset + f.vertices.index(vert)
fLine.append(str(v) + '/' + str(uv) + '/' + str(n))
vertices.extend(f.vertices)
indexOffset += len(f.vertices)
fLines.append(' '.join(fLine) + '\n')
# write to the file
stream.write('# shivshank\'s .obj optimizer\n')
stream.write('\n')
if len(normals) > 0:
stream.write('# normals\n')
for n in normals:
stream.write('vn ' + ' '.join(list(map(str, n))) + '\n')
stream.write('\n')
if len(uvs) > 0:
stream.write('# texcoords\n')
for i in uvs:
stream.write('vt ' + ' '.join(list(map(str, i))) + '\n')
stream.write('\n')
# output the vertices and faces
stream.write('# verts\n')
for v in vertices:
stream.write('v ' + ' '.join(list(map(str, v))) + '\n')
stream.write('\n')
stream.write('# faces\n')
for i in fLines:
stream.write(i)
stream.write('\n')
stream.write('\n')
return len(vertices), len(fLines)
def importVox(file):
""" --> a VoxelStruct from this .vox file stream """
# in theory this could elegantly be many functions and classes
# but this is such a simple file format...
# refactor: ? should probably find a better exception type than value error
vox = VoxelStruct()
magic = file.read(4)
if magic != b'VOX ':
print('magic number is', magic)
if userAborts('This does not appear to be a VOX file. Abort?'):
raise ValueError("Invalid magic number")
# the file appears to use little endian consistent with RIFF
version = int.from_bytes(file.read(4), byteorder='little')
if version != 150:
if userAborts('Only version 150 is supported; this file: '
+ str(version) + '. Abort?'):
raise ValueError("Invalid file version")
mainHeader = _readChunkHeader(file)
if mainHeader['id'] != b'MAIN':
print('chunk id:', mainId)
if userAborts('Did not find the main chunk. Abort?'):
raise ValueError("Did not find main VOX chunk. ")
#assert mainHeader['size'] == 0, "main chunk should have size 0"
# we don't need anything from the size or palette header!
# : we can figure out (minimum) bounds later from the voxel data
# : we only need UVs from voxel data; user can export palette elsewhere
nextHeader = _readChunkHeader(file)
while nextHeader['id'] != b'XYZI':
# skip the contents of this header and its children, read the next one
file.read(nextHeader['size'] + nextHeader['childrenSize'])
nextHeader = _readChunkHeader(file)
voxelHeader = nextHeader
assert voxelHeader['id'] == b'XYZI', 'this should be literally impossible'
assert voxelHeader['childrenSize'] == 0, 'why voxel chunk have children?'
seekPos = file.tell()
totalVoxels = int.from_bytes(file.read(4), byteorder='little')
### READ THE VOXELS ###
for i in range(totalVoxels):
# n.b., byte order should be irrelevant since these are all 1 byte
x = int.from_bytes(file.read(1), byteorder='little')
y = int.from_bytes(file.read(1), byteorder='little')
z = int.from_bytes(file.read(1), byteorder='little')
color = int.from_bytes(file.read(1), byteorder='little')
vox.setVoxel(Voxel(x, y, z, color))
# assert that we've read the entire voxel chunk
assert file.tell() - seekPos == voxelHeader['size']
# (there may be more chunks after this but we don't need them!)
#print('\tdone reading voxel data;', totalVoxels , 'voxels read ;D')
return vox
def _readChunkHeader(buffer):
id = buffer.read(4)
if id == b'':
raise ValueError("Unexpected EOF, expected chunk header")
size = int.from_bytes(buffer.read(4), byteorder='little')
childrenSize = int.from_bytes(buffer.read(4), byteorder='little')
return {
'id': id, 'size': size, 'childrenSize': childrenSize
}
def userAborts(msg):
print(msg + ' (y/n)')
u = input()
if u.startswith('n'):
return False
return True
def exportAll():
""" Uses a file to automatically export a bunch of files!
See this function for details on the what the file looks like.
"""
import os, os.path
with open('exporter.txt', mode='r') as file:
# use this as a file "spec"
fromSource = os.path.abspath(file.readline().strip())
toExportDir = os.path.abspath(file.readline().strip())
optimizing = file.readline()
if optimizing.lower() == 'true':
optimizing = True
else:
optimizing = False
print('exporting vox files under', fromSource)
print('\tto directory', toExportDir)
print('\toptimizing?', optimizing)
print()
# export EVERYTHING (.vox) walking the directory structure
for p, dirList, fileList in os.walk(fromSource):
pathDiff = os.path.relpath(p, start=fromSource)
outDir = os.path.join(toExportDir, pathDiff)
# REFACTOR: the loop should be moved to a function
for fileName in fileList:
# only take vox files
if os.path.splitext(fileName)[1] != '.vox':
print('ignored', fileName)
continue
print('exporting', fileName)
# read/import the voxel file
with open(os.path.join(p, fileName), mode='rb') as file:
try:
vox = importVox(file)
except ValueError as exc:
print('aborted', fileName, str(exc))
continue
# mirror the directory structure in the export folder
if not os.path.exists(outDir):
os.makedirs(outDir)
print('\tcreated directory', outDir)
# export a non-optimized version
objName = os.path.splitext(fileName)[0]
rawQuads = vox.toQuads()
with open(os.path.join(outDir, objName + '.obj'), mode='w') as file:
vCount, qCount = exportObj(file, rawQuads)
print('\texported', vCount, 'vertices,', qCount, 'quads')
if optimizing:
# TODO
continue
optiFaces = optimizedGreedyMesh(rawQuads)
bucketHash(optiFaces, *vox.getBounds())
with open(os.path.join(outDir, objName + '.greedy.obj'),
mode='w') as file:
exportObj(file, optiFaces)
def byPrompt():
import os, os.path, sys
from glob import glob
#### set output directory to first .vox file location
# ------------------------------------------------
####
u = os.path.abspath(sys.argv[0]).strip(os.path.basename(sys.argv[0]))
print(u)
#### drag & dropped files
# ---------------------
for i in sys.argv:
if i != sys.argv[0]:
print(i)
#### fully manual prompt ####
# -------------------
# print('Enter an output path:')
# u = input('> ').strip()
while not os.path.exists(u):
print('That path does not exist.')
print('Enter an output path:')
u = input('> ').strip()
outRoot = os.path.abspath(u)
print('\nStop file conversion to .obj? (type "y" or press "enter" to convert.)')
u = input('> ').strip()
if u.startswith('y'):
exit;
else:
try:
#while True:
#### grab files from prompt (uncomment lines below if needed)
# ----------------------
#print('Enter glob of export files (\'exit\' or blank to quit):')
#u = input('> ').strip()
#if u == 'exit' or u == '':
# break
#u = glob(u)
#### grab drag & dropped files
u = sys.argv
for f in u:
if f != sys.argv[0]:
print('reading VOX file', f)
with open(f, mode='rb') as file:
try:
vox = importVox(file)
except ValueError:
print('\tfile reading aborted')
continue
outFile = os.path.splitext(os.path.basename(f))[0]
outPath = os.path.join(outRoot, outFile+'.obj')
print('exporting VOX to OBJ at path', outPath)
with open(outPath, mode='w') as file:
exportObj(file, vox.toQuads())
except KeyboardInterrupt:
pass
if __name__ == "__main__":
profiling = False
try:
import cProfile
if profiling:
cProfile.run('exportAll()', sort='tottime')
else:
exportAll()
except OSError:
print('No instruction file found, falling back to prompt.')
byPrompt()
@TehJellyLord

This comment has been minimized.

Copy link

@TehJellyLord TehJellyLord commented Dec 20, 2019

When it says "drag and drop," what does that mean exactly? I'm a little bit of a noob, but I have tried dragging and dropping the physical .VOX file itself onto the python file, I've tried dragging the file into the IDLE window, and I've tried dragging and dropping the file into the window the program actually runs in and none have worked.

@awesomez

This comment has been minimized.

Copy link
Owner Author

@awesomez awesomez commented Dec 20, 2019

When it says "drag and drop," what does that mean exactly? I'm a little bit of a noob, but I have tried dragging and dropping the physical .VOX file itself onto the python file, I've tried dragging the file into the IDLE window, and I've tried dragging and dropping the file into the window the program actually runs in and none have worked.

I'm not sure what version of magica voxel you're using, but the .vox file should be dropped on the python script file (in Windows) itself and should create your files automatically for you. This functionality only works on version 3.5 of python or later (if I remember correctly), so make sure your python is updated to that version (or it won't work). Also, I'm not sure what permissions your directory has, but the .obj files should be written out to the same directory. If your script is located in Program Files directory in Windows, it won't work (by default) since administrator privileges are required, meaning you should try putting the python file someplace else.

If you're dragging and dropping the .vox file on the "xxx_PROMPT.py" script, then you'll have to go through the motions by answering all the prompts, then the script will output the file inside the folder you told it to. The automatic version just makes this happen without needing to go through all that.
The "_PROMPT" python file should just give you the prompt as to whether or not to perform the conversion, etc. in case you want to manually specify the filename yourself for some reason. There should be an instruction file telling the system where to output if you need to specify a different directory with the PROMPT file, but the PROMPT.py file just takes the input file as-is and puts it where the "instruction file" tells it to.

You can get more info on the "instruction file" in the main branch of this github repository. Since I don't use it myself, and since it has been a while since I modified this script, I don't remember all the details about it except for what it's function was intended to be.

Sorry I couldn't be much use at the moment -- Let me know if this helps at all!

@TehJellyLord

This comment has been minimized.

Copy link

@TehJellyLord TehJellyLord commented Dec 21, 2019

I figured it out... Python files on my computer are set to automatically open in PyCharm. I changed it back to IDLE and it worked perfectly. Lol

Thank you for your incites! If you hadn't mentioned my python version, I don't think I would have thought of that!

@awesomez

This comment has been minimized.

Copy link
Owner Author

@awesomez awesomez commented Dec 23, 2019

Not a problem! -- I'm just glad I could help somehow! ^___^

@xXAdolfHitlerXx

This comment has been minimized.

Copy link

@xXAdolfHitlerXx xXAdolfHitlerXx commented May 5, 2020

No instruction file found, falling back to prompt.
Enter an output path:

_
._. im not programmer

@awesomez

This comment has been minimized.

Copy link
Owner Author

@awesomez awesomez commented May 6, 2020

No instruction file found, falling back to prompt.
Enter an output path:

The "output path" is the path to where you want the .obj file to be exported to -- i.e. C:\Windows\Users\yourname\Documents
NOTE: Don't use the file ending in "_PROMPT.py" file if you want to output to the current directory.

Next -- Did you drag/drop the .vox file onto one of the .py scripts?
If you did, and you've pressed Enter, and this message still appears, there are three possibilities:

1.) Old Python version prior to version 3.5 (I think?)
2.) Magica Voxel version too new
3.) Use / instead of \ for path to output directory to see if that works

Been a while since I've played with this script, but you don't need an "output directory" when you have an "instruction file" (which is described a little in the main branch of this repository).

FINAL NOTE:

You'd want to use "vox_to_obj_AUTO.py" (the first script above) most of the time (rather than "vox_to_obj_PROMPT.py", which gives you a prompt for the output directory unless an "instruction file" exists in the same folder as the script)

@raavek

This comment has been minimized.

Copy link

@raavek raavek commented Jan 25, 2021

This script is awesome thank you so much!

I've been really stumped and this script is really helping. Im not a programmer but I got this script to output just fine.
I do have a question/huge favor/guidance if I could ask. Is there any way to export an obj file that preserves internal voxel structure as well, not just the external surface? So the export would have an object containing internal voxel structure with color palette applied?

@awesomez

This comment has been minimized.

Copy link
Owner Author

@awesomez awesomez commented Jan 25, 2021

This script is awesome thank you so much!

I just adapted it to make it more user-friendly -- thanks though! :)

Is there any way to export an obj file that preserves internal voxel structure as well, not just the external surface? So the export would have an object containing internal voxel structure with color palette applied?

The "importVox(file)" method does exactly what you want -- it returns a VoxelStruct class / set of data, which is what you would want to output to a file (such as JSON), which you'd have to do yourself in Python atm. Finally, you'd need to import that JSON into whatever app you want that data for.

I know you said you're not a programmer, but since I don't have time to make it myself, you might want to give it a go. On the plus side - Python is very straightforward, and instead of curly brackets, etc to define "groups" of code -- it uses TAB spacing. That said, if you end up making a version of this script that outputs JSON (which, IMO, is the best raw-text data exchange format available), please let me know. It would be very nice to have a JSON version of this script in this repository for anyone interested in using the raw voxel data structure (instead of just the .obj) to use later in another application.
Like I said, it should be VERY straightforward to go this route (you're really just exporting the "VoxelStruct" class's individual properties / data as variables, one at a time, for each individual property like x, y, z, color -- and writing that to a simple text file using the JSON format -- that's it). To do my part (and give back to your efforts) -- if you do decide to take it on, and you have any issues implementing it (or questions about how or what to do or where to look), I don't mind walking you through getting a JSON export up and running (a little bit at a time), but I don't think there are too many things that could go wrong if you glance at the basics of Python and study that function/method I pointed out (importVox) a tiny bit. Most of the stuff you'd need to do exists in the script already (including writing to a file -- except this is binary, not plaintext, so you'd have to look into writing to a plain text file). Other than that, it should be pretty simple. Please let me know if you plan to do this (and if you run into any issues, assuming you do!) 👍

@raavek

This comment has been minimized.

Copy link

@raavek raavek commented Jan 25, 2021

This script is awesome thank you so much!

I just adapted it to make it more user-friendly -- thanks though! :)

Is there any way to export an obj file that preserves internal voxel structure as well, not just the external surface? So the export would have an object containing internal voxel structure with color palette applied?

The "importVox(file)" method does exactly what you want -- it returns a VoxelStruct class / set of data, which is what you would want to output to a file (such as JSON), which you'd have to do yourself in Python atm. Finally, you'd need to import that JSON into whatever app you want that data for.

I know you said you're not a programmer, but since I don't have time to make it myself, you might want to give it a go. On the plus side - Python is very straightforward, and instead of curly brackets, etc to define "groups" of code -- it uses TAB spacing. That said, if you end up making a version of this script that outputs JSON (which, IMO, is the best raw-text data exchange format available), please let me know. It would be very nice to have a JSON version of this script in this repository for anyone interested in using the raw voxel data structure (instead of just the .obj) to use later in another application.
Like I said, it should be VERY straightforward to go this route (you're really just exporting the "VoxelStruct" class's individual properties / data as variables, one at a time, for each individual property like x, y, z, color -- and writing that to a simple text file using the JSON format -- that's it). To do my part (and give back to your efforts) -- if you do decide to take it on, and you have any issues implementing it (or questions about how or what to do or where to look), I don't mind walking you through getting a JSON export up and running (a little bit at a time), but I don't think there are too many things that could go wrong if you glance at the basics of Python and study that function/method I pointed out (importVox) a tiny bit. Most of the stuff you'd need to do exists in the script already (including writing to a file -- except this is binary, not plaintext, so you'd have to look into writing to a plain text file). Other than that, it should be pretty simple. Please let me know if you plan to do this (and if you run into any issues, assuming you do!) 👍

Phew. Ok, Ive always wanted to start learning python. I will give this a go. Ive needed to learn about JSON as well so this should be a good challenge. I've never coded anything outside of an arduino or a basic webpage in my life. If I do get this coded when I put the code up here I will do something called a fork off your code? Im really new and basic at this sorry lol. You've lit a fire in me and now I really want to do this lol

@awesomez

This comment has been minimized.

Copy link
Owner Author

@awesomez awesomez commented Jan 25, 2021

Phew. Ok, Ive always wanted to start learning python. I will give this a go. Ive needed to learn about JSON as well so this should be a good challenge. I've never coded anything outside of an arduino or a basic webpage in my life.

You should be set then -- Python is extremely useful/versatile, but it is easier to learn than programming webpages and Arduino stuff!!
And JSON is just a great (highly-useful!) data-interchange format that is a lot like XML, but smaller and less verbose -- and makes A LOT more sense imo. It's not much different than CSS syntax for webpages -- so extremely basic.

If I do get this coded when I put the code up here I will do something called a fork off your code? Im really new and basic at this sorry lol. You've lit a fire in me and now I really want to do this lol

That's great to hear! -- It sounds like you'd benefit from this experience greatly!

If you want to make a "https://pastebin.com/" link when you're done (i.e. create a link on that site and paste it here), I can add your new python script(s) to this (current) repository for you if you'd like. That way everything will stay in one place, and you can always find it with the other .obj scripts in the future. This means you wouldn't have to mess with github (unless you want to -- and in that case, a fork would let you add onto/develop your script(s) further in the future if you wish). If future development isn't what you're after, pastbin and simply posting the link to your script here to be added to this repository when you're done would still allow you to fork it later, assuming you wanted to make changes. However, I offered to keep your changes here to keep all the new scripts in one place (by keeping them in the same area). I can do this in the future for you too, assuming you decide to make future changes (if you wish). I am subscribed here, and active, so I don't mind the github bit.

But yeah -- I'm glad you're considering learning this stuff! -- It's extremely useful. After all, Voxels are really cool, and Magica Voxel is a pretty nice tool to create them with. Having a straightforward JSON interchange format from .vox files is definitely handy for working with basic voxel data, and currently there are only paid options available for such a simple script -- so you'd be doing many others a favor too. :)

Good luck -- and let me know if you run into any issues! -- I'll be glad to help! :)

@raavek

This comment has been minimized.

Copy link

@raavek raavek commented Jan 25, 2021

Do you have an email address or way to contact you? Ive been staring at this code for hours and I have some questions but I dont know who I should ask them too. I drew a picture to try to make sense of what I was looking at. I really do want to understand what is going on here.

https://i.imgur.com/decy4QC.png

@awesomez

This comment has been minimized.

Copy link
Owner Author

@awesomez awesomez commented Jan 25, 2021

Do you have an email address or way to contact you? Ive been staring at this code for hours and I have some questions but I dont know who I should ask them too. I drew a picture to try to make sense of what I was looking at. I really do want to understand what is going on here.

https://i.imgur.com/decy4QC.png

You can contact me here -- it goes straight to my email. No worries.

I was looking at your questions and to answer your most basic ones -- The part you've boxed-out in red is building a string.
Something like "self.uv = uv" is actually "building" the 'object' by defining the relationship between "uv" and "self" (initialized in the init part) as an 'alias' (that is, when referencing "self.uv", you are actually referencing the sole "uv" that was initialized in the init list of arguments -- not "self" which was also defined there). You are essentially "building" up an "object" at this step, rather than simply assigning values (as you would in other languages). You, instead, are assigning references to the original data stores -- i.e. whatever goes in "self.uv" ends up inside the "uv" container, but whatever object you're dealing with gets stored in the "self" data container. That is, you're not actually referencing "uv" when you go "self.uv = uv" -- you're assigning the data container (initialized in init of course) to the phrase "self.uv" so that when you reference that "uv" data container, you're doing it with "self.uv" as the label for that specific object's data.

Since you're defining a class here (and not really using the class yet), this is all "setup" work for your data. You're telling the computer where you're going to put the data -- and how to do that -- but not necessarily when it needs to be done. At least not until you call one of the methods defined by the "def" keyword.

I know that was a bit long-winded, but maybe that makes a little more sense?

Either way -- You might be getting ahead of yourself.

I suggest starting with the VoxelStruct class, as it is more straightforward, and ultimately where you want to be.
You only need to grab the properties out of that VoxelStruct you've imported (in "importVox", the struct is named "vox" at the very beginning of the function/method call, but since it returns "vox" as a VoxelStruct, you can call your VoxelStruct variable whatever you want. After you call "voxelstruct = importVox(...)" for example, you can now access the properties inside your VoxelStruct called "voxelstruct" and write them to a file if you wish. You would only need to create a new function/method to call this -- OR erase and use the "exportObj" function/method to automatically output the JSON file. There is some useful info in that function/method too, so back it up before you delete it in case you want to see how that file is written.

Please let me know if this helps give you a bit more direction. Definitely watch some python videos if you can though, as it's not too hard -- just a little different because you have to "build" your data objects (for more complex behaviors/referencing). In your case though -- remember, all you're doing is grabbing the output from an already "imported" VoxelStruct, and writing those values somewhere else in the "exportObj" routine (which you might call "exportVox" and change any references from "exportObj" to "exportVox" so that when you drag and drop your .vox file on there, a JSON file is output instead of a new .obj file. :)

@raavek

This comment has been minimized.

Copy link

@raavek raavek commented Jan 26, 2021

I haven't started writing code yet but I have been studying python a lot and looking at this code. Still getting my brains around this. An update to where my thought process is:

https://i.imgur.com/aFt0GQ5.png

edit: Still tearing through the code. I found some pieces further down the list that I think I need to focus on. I will update when I can. This is kinda fun.

@awesomez

This comment has been minimized.

Copy link
Owner Author

@awesomez awesomez commented Jan 26, 2021

I noticed you're focusing on faces.
If you're trying to do something based on face geometry (i.e. put something on a particular 'face' of a voxel), then yes, the face thing is important. However, if you're just wanting the center position of the voxel itself (that is, the local 0,0,0 position of the individual voxel), you should look elsewhere (i.e. the 3d voxel index).
The reasoning here is because the actual index can be used to calculate the location of the face in physical space (from the center of each voxel) -- and even checking of the voxel should have a face in a particular position (say, behind it). The script itself uses 0-1 to calculate front/back (for example), but .vox files use -0.5-0.5 to calculate which side of a box a face is on (for front/back , left/right , top/bottom respectively). The 0-1 method avoids dealing with signage issues that might arise with multiplication with numbers like -0.5 for example, assuming you want to reference a voxel (and its faces) directly through math calculations. You can simply subtract 0.5 from 0 to get the -0.5 spatial position (for the back), or subtract 0.5 from 1 to get 0.5 (for the front of a cube), for example.

Voxels themselves aren't terribly complex -- you mainly just need a clear forward axis (z), a flat master list of voxels (i.e. count all voxels across each axis, store them to said master list while skipping any you've already found on a previous axis in order to avoid repeating them).
If you know your maximum grid size (assumed to be 256x256x256 in the script), you can go all the way to 255 on every other axis, then get that last 'plane' (possibly the ground level of voxels at the base) and scoop them up in either the first (or the final) loop in order to prevent the repetition.
From this point on, you just use the 'center' of each voxel offset to figure out where the faces are physically in the world in whatever direction you like (and store/get data from there if you wish). If the origin of your model is at the center of the base, this makes it easier (as you don't have to deal with negative values on the grid). This means the grid itself should always be a power of 2 though, in order to make those line up. Your smallest grid will be 2x2x2 at minimum. As weird as this sounds -- it is quite common believe it or not.

Hopefully that gives you some "behind the scenes" insight into this script's thought process. :)

Good luck! 👍

@raavek

This comment has been minimized.

Copy link

@raavek raavek commented Jan 26, 2021

I see what you are saying! I dont really want to change any specific quad/face on a voxel I would like each voxel to be monotone in color, but have all voxels retain the palette. But I do want all the internal voxels faces to be present within the render. Te reason I was looking at the faces was to try to figure out how the script determines which faces get rendered or not. I see what you are saying about a master 'position' list for voxel placement. I think I saw somewhere in the code that 1 voxel becomes 0.1m³ square in an obj file.

I think if I can find that master position index like you said and then figure out how the code determines whether to render a particular voxel or not I think then I can start figuring out how to edit the code to fit the desired purpose.

Since it is helping me to visualize this, heres another pic in cause you want a "behind the scenes" into a complete novice's first time digging into code lol.

https://i.imgur.com/0RTnF8q.png

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment