Skip to content

Instantly share code, notes, and snippets.

@pink-vertex
Last active April 22, 2018 09:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pink-vertex/74f52e2784d7d17f19f7f4900f98cc7f to your computer and use it in GitHub Desktop.
Save pink-vertex/74f52e2784d7d17f19f7f4900f98cc7f to your computer and use it in GitHub Desktop.
import os
import gzip
from ctypes import *
# NOTE Select a single cube extrude it 1 block and execute "/pastebrush py" or "/pastebrush house"
# /pastebrush does not! work with block range selections (blue selection box) and
# only overrides cubes in the selection according to some rules
# i.e. cubes of air material do not override/replace existing cubes
# orientation up 4(or down 5) means dimension is 4 >> 1 == 2, row R[2] == 0, column C[2] == 1, depth D[2] == 2
# dimension coord (orient & 1) does matter (see blockcube(...)) and determines direction
# loop depth ( loop column ( loop row ) )
# check alignment ? does Structure use it for readinto ? (yes, it does)
# may be fixed with class attribute _pack_ = 1 -- most structures don't need padding anyway
# NOTE might implement operations like mirror/flip, rotate etc for cubes/block ranges
# define block data or read it from an existing obr brush
# and apply transformations to compose the new brush
# reuse cubes that are structurally identical
# sample geometry using the finest raster/resolution (max subdivision) and simplify afterwards
# by replacing children with single parent cube
# TODO better use integer vector/array class instead of python lists
# R = 1, 2, 0
# C = 2, 0, 1
# D = 0, 1, 2
#define cubeedge(c, d, x, y) (c.edges[(( d << 2 ) + ( y << 1 ) + ( x ))])
#define edgeget(edge, coord) ((coord) ? (edge)>>4 : (edge)&0xF)
# v = T(edgeget(cubeedge(c, 0, y, z), x)
# edgeget(cubeedge(c, 1, z, x), y)
# edgeget(cubeedge(c, 2, x, y), z)
class Header(Structure):
_fields_ = [( 'magic', c_char*4 ),
( 'version', c_int )]
class Block3(Structure):
_fields_ = [( 'o', c_int*3 ),
( 's', c_int*3 ),
( 'grid', c_int ),
( 'orient', c_int )]
def __init__(self, o=(0 ,0 ,0), s=(1, 1, 1), grid=8, orient=4):
super().__init__(o, s, grid, orient)
def size(self):
r = 1
for co in self.s:
r *= co
return r
Edges = c_ubyte*12
Textures = c_uint16*6
def read_struct(stream, cls):
i = cls()
stream.readinto(i)
return i
# ------------------------------------------------------------------------------
class Cube:
EMPTY = Edges(*(0x00 for i in range(12)))
SOLID = Edges(*(0x80 for i in range(12)))
TEX_DEFAULT = Textures(*(0 for i in range(6)))
def __init__(c):
c.material = 0
c.children = None # -Z first, then -Y, -X which means specific cube can be retrieved by 4*Z + 2*Y + X just like cubeedge(...)
c.textures = None
c.edges = None
@classmethod
def from_stream(cls, stream):
c = Cube()
c.material = stream.read(1)[0]
if c.material == 0xFF:
c.children = [Cube.from_stream(stream) for i in range(8)]
else:
c.material |= stream.read(1)[0] << 8
c.edges = read_struct(stream, Edges)
c.textures = read_struct(stream, Textures)
return c
def serialize(c, stream):
stream.write((c.material & 0xFF).to_bytes(1, "little"))
if c.material == 0xFF:
for child in c.children:
child.serialize(stream)
return
stream.write((c.material >> 8).to_bytes(1, "little"))
stream.write(c.edges)
stream.write(c.textures)
def corner(c, vec):
return Corner(vec, c.edges)
def subdivide(c, edges, mat=0, tex=None):
if c.children: return
c.material = 0xFF
c.children = [None] * 8
for i in range(8):
child = Cube()
child.material = mat
child.edges = edges
child.textures = tex or c.textures or Cube.TEX_DEFAULT
c.children[i] = child
def discard_children(c, mat=0):
if not c.children: return
c.material = mat
c.children = None
c.edges = Cube.SOLID
c.textures = c.textures or Cube.TEX_DEFAULT
class Corner:
def __init__(self, vec, edges):
self.i = [[0, 0], [0, 0], [0, 0]]
self.e = edges
x, y, z = vec
self.i[0][0] = self.cubeedge(0, y, z)
self.i[1][0] = self.cubeedge(1, z, x)
self.i[2][0] = self.cubeedge(2, x, y)
for n, co in enumerate(vec):
self.i[n][1] = co
@classmethod
def cubeedge(cls, d, r, c):
return r + (c << 1) + (d << 2)
def __getitem__(self, n):
if not isinstance(n, int): raise TypeError
e = self.e[self.i[n][0]]
return e >> 4 if self.i[n][1] else e & 0x0F
def __setitem__(self, n, val):
if not isinstance(n, int): raise TypeError
i = self.i[n][0]
self.e[i] = ( (self.e[i] & 0x0F) + (val << 4) if self.i[n][1] else
(self.e[i] & 0xF0) + (val) )
def __getattr__(self, attr):
try: n = "xyz".index(attr)
except ValueError: raise AttributeError
return self.__getitem__(n)
def __setattr__(self, attr, val):
try: n = "xyz".index(attr)
except ValueError: return super().__setattr__(attr, val)
self.__setitem__(n, val)
# ------------------------------------------------------------------------------
def edge(edges, vec, axis):
v = list(vec)
result = [None] * 2
result[0] = Corner(v, edges)
v[axis] = v[axis] ^ 1
result[1] = Corner(v, edges)
return result
def face(edges, normal, co):
v = [0, 0, 0]
v[normal] = co
r = (normal + 1) % 3
c = (normal + 2) % 3
result = [None] * 4
for i in (0, 1, 3, 2):
v[r] = i & 1
v[c] = i >> 1
result[i] = Corner(v, edges)
return result
def texture(val):
if isinstance(val, int):
return Textures(*(val for i in range(6)))
return Textures(*val)
def cubegen(sx, sy, sz, callback):
cubes = [Cube() for i in range(sx*sy*sz)]
index = 0
for z in range(sz):
for y in range(sy):
for x in range(sx):
callback(x, y, z, cubes[index])
index += 1
return cubes
# ------------------------------------------------------------------------------
def read_obr(filepath):
with gzip.open(filepath, "rb") as stream:
h = read_struct(stream, Header)
assert h.magic == b'OEBR' and h.version == 0
block = read_struct(stream, Block3)
cubes = [Cube.from_stream(stream) for i in range(block.size())]
return block, cubes
def write_obr(filepath, block, cubes):
with gzip.open(filepath, "wb") as stream:
stream.write(Header(b'OEBR', 0))
stream.write(block)
for cube in cubes:
cube.serialize(stream)
# ------------------------------------------------------------------------------
def test_pyramid(BASE):
BASE |= 1 # needs to be an odd number!
HEIGHT = BASE # odd number 2*n + 1, every 2 layers diameter decreases by 2 units, edges collapse at single top block
CENTER = BASE >> 1
TEX = texture(10)
b = Block3(s=(BASE, BASE, HEIGHT))
def push_edge(c, n, b, z, val):
v = [0, 0, z]
v[n] = b
e = edge(c.edges, v, n ^ 1)
e[0][n] = val
e[1][n] = val
def push(is_odd, c, n, b):
if not is_odd:
push_edge(c, n, b, 1, 4)
return
push_edge(c, n, b, 0, 4) # bottom edge z=0
push_edge(c, n, b, 1, 0 if b else 8) # top edge z=1
def cube_data(x, y, z, c):
c.textures = TEX
# two passageways crossing each other
if abs(CENTER - x) < 2 and z < 4: c.edges = Cube.EMPTY; return
if abs(CENTER - y) < 2 and z < 4: c.edges = Cube.EMPTY; return
d = max(abs(CENTER - x), abs(CENTER - y)) # distance to the center
h = (BASE - z) >> 1 # radius depending on the height decreasing every 2 layers
b = z & 1 # even/odd layer
if d < h: c.edges = Cube.SOLID
elif d > h: c.edges = Cube.EMPTY
else:
c.edges = Edges.from_buffer_copy(Cube.SOLID)
for n, co in enumerate((x, y)):
if CENTER - co == h: push(b, c, n, 0)
if CENTER - co == -h: push(b, c, n, 1)
cubes = cubegen(BASE, BASE, HEIGHT, cube_data)
filepath_out = os.path.expanduser("~/.sauerbraten/packages/brush/py.obr")
write_obr(filepath_out, b, cubes)
# ------------------------------ Brush Classes ---------------------------------
def in_bounds(v, origin, size):
return all(o <= co < o + s for co, o, s in zip(v, origin, size))
class BlockBrush:
def __init__(b, o, s, fill=True):
b.o = o
b.s = s
b.f = Cube.SOLID if fill else Cube.EMPTY
def cube_data(b, v, cube):
cube.discard_children()
cube.edges = b.f
class WindowBrush:
TEX_FRAME = texture(4)
def __init__(b, o, s, orient):
b.o = o
b.s = s
b.orient = orient # bottom, up, left, right (from top view in xy plane)
def move_copy(b, distance):
copy = WindowBrush(list(b.o), b.s, b.orient)
dim = b.orient >> 1
copy.o[dim] += b.s[dim] + distance
return copy
def cube_data(b, v, cube):
cube.edges = Cube.EMPTY
dc = b.orient & 1
r = b.orient >> 1
c = r ^ 1
z = ( 0 if v[2] == b.o[2] else
1 if v[2] == b.o[2] + b.s[2] - 1 else
-1 )
s = ( 0 if v[r] == b.o[r] else
1 if v[r] == b.o[r] + b.s[r] - 1 else
-1 )
if z == s == -1: return
cube.subdivide(Cube.EMPTY)
def func(cube, z, s, r, c, dc):
for i in range(4):
i, j = (i >> 1), i & 1
if z == i: cube.children[(z << 2) + (dc << c) + (j << r)].edges = Cube.SOLID
if s == i: cube.children[(j << 2) + (dc << c) + (s << r)].edges = Cube.SOLID
for i in range(4):
i, j = (i >> 1), i & 1
if z == i:
child = cube.children[(z << 2) + (dc << c) + (j << r)]
# subdivide again to get a thinner window frame
child.subdivide(Cube.EMPTY, 0, WindowBrush.TEX_FRAME)
cs = ( 0 if s == j == 0 else
1 if s == j == 1 else
-1 )
func(child, z, cs, r, c, dc)
if s == i:
child = cube.children[(j << 2) + (dc << c) + (s << r)]
# Cube.subdivide will only do anything if there aren't any children, yet
child.subdivide(Cube.EMPTY, 0, WindowBrush.TEX_FRAME)
cz = ( 0 if z == j == 0 else
1 if z == j == 1 else
-1 )
func(child, cz, s, r, c, dc)
class RampBrush:
def __init__(self, o, s, dim, dc, stepsize):
self.o = o
self.s = s
self.dim = dim
self.dc = dc
assert 8 % stepsize == 0 # stepsize should be divisor of 8
self.steps = 8 // stepsize
self.edges = [None] * self.steps
for i in range(self.steps):
edges = Edges.from_buffer_copy(Cube.SOLID)
self.edges[i] = edges
idx = [0, 0, 1]
idx[dim] = dc
e = edge(edges, idx, dim ^ 1)
e[0][2] = i*stepsize
e[1][2] = i*stepsize
idx[dim] = dc ^ 1
e = edge(edges, idx, dim ^ 1)
e[0][2] = (i+1)*stepsize
e[1][2] = (i+1)*stepsize
def cube_data(self, v, cube):
h = ( v[2] - self.o[2] )
d = ( v[self.dim] - self.o[self.dim] if not self.dc else
-v[self.dim] + self.o[self.dim] + self.s[self.dim] - 1)
i = d % self.steps
d = d // self.steps
cube.edges = ( self.edges[i] if d == h else
Cube.SOLID if d > h else
Cube.EMPTY )
class StairsBrush:
def __init__(self, o, s, dim, dc):
self.o = o
self.s = s
self.dim = dim
self.dc = dc
self.textures = texture(4)
self.edges = Edges.from_buffer_copy(Cube.SOLID)
v = [0, 0, 0]
v[dim] = dc ^ 1
e = edge(self.edges, v, dim ^ 1)
e[0][dim] = (0, 8)[dc]
e[1][dim] = (0, 8)[dc]
def cube_data(self, v, cube):
b = self.dc
i = self.dim
j = self.dim ^ 1
h = v[2] - self.o[2]
d = ( v[self.dim] - self.o[self.dim] if not self.dc else
-v[self.dim] + self.o[self.dim] + self.s[self.dim] - 1)
cube.textures = self.textures
if h == d:
cube.subdivide(Cube.SOLID)
cube.children[4 + (b << i) + (0 << j)].edges = Cube.EMPTY
cube.children[4 + (b << i) + (1 << j)].edges = Cube.EMPTY
elif h == d - 1:
cube.edges = self.edges
else:
cube.edges = Cube.EMPTY
#-------------------------------------------------------------------------------
def test_house(BASE):
B = BASE
N = 4 # Number of Layers
LH = 6 # Layer Height
WI = 3 # Window Inset
WW = 5 # Window Width
WE = 1 # Window Elevation
WH = LH - 3 # Window Height
WN = 2 # Number of Windows
WG = 4 # Window Gap
SW = 3 # Stairs Width
TW = 10 # Wall Texture
TF = 4 # Floor Texture
HEIGHT = N * LH
TEX = texture((TW, TW, TW, TW, TF, TF))
b = Block3(s=(BASE, BASE, HEIGHT))
brushes = []
def bb(o, s, f): brushes.append(BlockBrush(o, s, f))
def sb(o, s, dim, dc): brushes.append(StairsBrush(o, s, dim, dc))
def wb(o, s, orient, n, gap):
brush = WindowBrush(o, s, orient)
brushes.append(brush)
for i in range(1, n):
brush = brush.move_copy(gap)
brushes.append(brush)
for i in range(N): # Layer
bb( o=( 0, 0, LH*i ), s=( B , B , LH ), f=True )
bb( o=( 1, 1, LH*i ), s=( B-2, B-2, LH-1 ), f=False )
# Windows
for j in range(4):
o = ( (WI, WI, 0, B-1)[j], (0, B-1, WI, WI)[j], LH*i + WE )
s = ( (WW, WW, 1, 1)[j], (1, 1, WW, WW)[j], WH )
wb(o, s, j, WN, WG)
# Staircase
if i & 1: sb( o=( 4, B-7, LH*i ), s=( LH, SW, LH ), dim=0, dc=1 )
else : sb( o=( 5, B-4, LH*i ), s=( LH, SW, LH ), dim=0, dc=0 )
bb( o=( (B >> 1) - 1, 0, 0 ), s=( (B & 1) + 2, 1, 4 ), f=False ) # Door
brushes.append(RampBrush(
o=( B-5, 1, 0 ), s=( 4, BASE - 2, LH ), dim=1, dc=1, stepsize=2))
def cube_data(x, y, z, c):
c.textures = TEX
for brush in brushes:
if not in_bounds((x, y, z), brush.o, brush.s): continue
brush.cube_data((x, y, z), c)
cubes = cubegen(BASE, BASE, HEIGHT, cube_data)
filepath_out = os.path.expanduser("~/.sauerbraten/packages/brush/house.obr")
write_obr(filepath_out, b, cubes)
if __name__ == "__main__":
test_pyramid(31)
test_house(20)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment