Last active
April 22, 2018 09:37
-
-
Save pink-vertex/74f52e2784d7d17f19f7f4900f98cc7f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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