yeah, thats it. the annoying bit is that textures are square. so if you need to store vertices and reference them by indeex, you need to do a bit of math to pick the right texel. i’ve been using this function here:
ivec2 idx2coord_row(int idx, ivec2 imsize){
return ivec2(idx % imsize.x, idx / imsize.x);
}
It takes a 1D index and converts it to a 2D coordinate to use access a image pixel.
but recently, I came up with this other one that uses a chunked indexing of 16x16 blocks, and within each block it overlays a small z-order curve instead of a simple row-by-row. I was hoping to improve cache performance by doing this but It didn’t change much in my tests.
uint morton_decode8(uint x){
x = (x & 0x44u) >> 1 | (x & 0x11u);
x = (x & 0x30u) >> 2 | (x & 0x03u);
return x;
}
ivec2 idx2coord_block(uint idx, ivec2 imsize){
uint w = uint(imsize.x);
uint block_id = idx >> 8u;
uint item_id = idx & 0xffu;
ivec2 item_offset = ivec2(morton_decode8(item_id), morton_decode8(item_id >> 1u));
uint wblock = w >> 4;
uint w_mask = wblock - 1u; // assumes imsize.x is a power of two
uint w_shift = uint(findLSB(wblock));
return (ivec2(block_id & w_mask, block_id >> w_shift) << 4u) + item_offset;
}
So, I also make some helper macros to wrap image access funcctions using this, so the final pixel data access code looks like this:
# define IDX2COORD(im, idx) idx2coord_block(idx, imageSize(im))
# define IDX2COORD_TEX(im, idx) idx2coord_block(idx, textureSize(im))
uint morton_decode8(uint x){
x = (x & 0x44u) >> 1 | (x & 0x11u);
x = (x & 0x30u) >> 2 | (x & 0x03u);
return x;
}
ivec2 idx2coord_block(uint idx, ivec2 imsize){
uint w = uint(imsize.x);
uint block_id = idx >> 8u;
uint item_id = idx & 0xffu;
ivec2 item_offset = ivec2(morton_decode8(item_id), morton_decode8(item_id >> 1u));
uint wblock = w >> 4;
uint w_mask = wblock - 1u; // assumes imsize.x is a power of two
uint w_shift = uint(findLSB(wblock));
return (ivec2(block_id & w_mask, block_id >> w_shift) << 4u) + item_offset;
}
ivec2 idx2coord_block(int idx, ivec2 imsize){
return idx2coord_block(uint(idx), imsize);
}
# define load(im, idx) imageLoad(im, IDX2COORD(im, idx))
# define store(im, idx, data) imageStore(im, IDX2COORD(im, idx), data)
# define fetch(im, idx) texelFetch(im, IDX2COORD_TEX(im, idx), 0)
# define atomic_min_idx(im, idx, data) imageAtomicMin(im, IDX2COORD(im, idx), data)
# define atomic_max_idx(im, idx, data) imageAtomicMax(im, IDX2COORD(im, idx), data)
# define atomic_add_idx(im, idx, data) imageAtomicAdd(im, IDX2COORD(im, idx), data)
# define atomic_and_idx(im, idx, data) imageAtomicAnd(im, IDX2COORD(im, idx), data)
# define atomic_or_idx(im, idx, data) imageAtomicOr(im, IDX2COORD(im, idx), data)
# define atomic_xor_idx(im, idx, data) imageAtomicXor(im, IDX2COORD(im, idx), data)
# define atomic_compswap_idx(im, idx, compare, data) imageAtomicCompSwap(im, IDX2COORD(im, idx), compare, data)
# define atomic_exchange_idx(im, idx, data) imageAtomicExchange(im, IDX2COORD(im, idx), data)
Thats just one part of the equation though, I’l write some more detailed breakdown later but, I also had to figure out how to upload and retrieve texture that other than floats, I made a mini-transpiler to generate UBO struct definitions and python dtypes and a kind of preprocessor that spilices GLSL code together and builds it just in time to I can write inline GLSL in the middle of my python and have it kind of just work.
for example, the code that stores mesh data as textures looks like this:
from .shader_utils import *
import numpy as np
compute_inline_tmesh = compute_inline_partial(local_size=(128, 1, 1), include=['mesh'])
class TMesh:
def __init__(self, verts, tris):
assert verts.dtype == np.float32
assert tris.dtype == np.uint32
verts = verts.reshape(-1, 3)
tris = tris.reshape(-1, 3)
self.n_verts = len(verts)
self.n_tris = len(tris)
size = length_to_square(self.n_verts)
new_verts = np.zeros((np.prod(size), 4), dtype=np.float32)
new_verts[:self.n_verts, :3] = verts
self.verts = Texture.from_array(new_verts, format='RGBA32F', size=size)
size = length_to_square(self.n_tris)
tris_input = np.zeros((np.prod(size), 4), dtype=np.uint32)
tris_input[:self.n_tris, :3] = tris
self.tris = Texture.from_array(tris_input, format='RGBA32UI', size=size)
self.normals = Texture(size, format='RGBA32F')
# flag degenerate triangles in case needed.
compute_inline_tmesh(self.n_tris,
verts=self.verts.R,
tris=self.tris,
code='''//glsl
uint idx = gl_GlobalInvocationID.x;
uvec4 tri = load(tris, idx);
vec3 va = load(verts, tri.x).xyz;
vec3 vb = load(verts, tri.y).xyz;
vec3 vc = load(verts, tri.z).xyz;
vec3 x = cross(vb - va, vc - va);
float d = dot(x, x);
tri.w = int((d == 0.0 || isnan(d) || isinf(d)));
store(tris, idx, tri);
''')
@classmethod
def from_object(cls, ob):
mesh = ob.data
mesh.calc_loop_triangles()
obverts = mesh.vertices
obtris = mesh.loop_triangles
verts = np.zeros((len(obverts) * 3), dtype=np.float32)
tris = np.zeros((len(obtris) * 3), dtype=np.uint32)
obverts.foreach_get('co', verts)
obtris.foreach_get('vertices', tris)
return cls(verts, tris)