Procedural Content¶
Four modules of the acron package serve games whose worlds are made by
code rather than shipped as assets: acron.noise makes deterministic
fields, acron.gpu turns numpy arrays into textures, acron.geom
builds meshes, and acron.quads keeps a pool of quads in one vertex
buffer. They use numpy; import each module explicitly.
Deterministic noise¶
acron.noise is value noise built on exact 32-bit integer hashing, not
on fract(sin(...)), so the same seed gives the same world on any machine.
A terrain baked from these functions is sampled identically by the renderer
and the physics.
Every function takes floats or numpy arrays; pass arrays to evaluate a whole field at once:
import numpy as np
from acron import noise
xs, zs = np.meshgrid(np.arange(512) / 64.0, np.arange(512) / 64.0)
height = noise.fbm(xs, zs, octaves=5, seed=7) # 0..1
rims = noise.ridged(xs, zs, octaves=4, seed=7) # sharp crests
hash2i(x, y, seed) is the hash itself, in [0, 1); vnoise is 2D value
noise with a quintic fade; fbm and ridged sum octaves (lac and
gain set the lacunarity and the falloff); pvn(x, y, per, seed) is
periodic, for a tile that repeats without a seam. clamp, sstep,
lerp and dmod are the small helpers the functions use.
RNG is a seeded mulberry32 stream: call it for a float in [0, 1), or
uniform(lo, hi). Give each consumer (scatter, loot, effects) a stream of
its own, so that one never shifts another’s rolls.
rng = noise.RNG(1234)
x = rng.uniform(-50.0, 50.0)
Arrays to textures¶
acron.gpu is the one place where numpy arrays become Texture
objects, and it handles the two things that are easy to get wrong without
noticing:
Row order. The engine’s RAM images start at the bottom row. A computed field indexed
[z, x], with z growing the way texture v does, is uploaded as it is (float_texture,half_texture,byte_texture,rgb_texture,rgba_texture). A drawn image, painted top row first, is flipped on the way in: useimage_texturefor it.Mipmaps.
float_texture(..., mips=[base, level1, ...])uploads the chain you computed, for a shader that samples specific pre-filtered levels.
from acron import gpu
height_tex = gpu.float_texture("height", height.astype(np.float32),
wrap=True)
terrain.set_shader_input("u_height", height_tex)
half_texture makes a half-float texture for a field that is re-uploaded
often, and update_half_rows(tex, field, z0, z1) pushes only the rows that
changed. vertex_view(vdata, floats_per_vertex) is a writable numpy view
of a single-array GeomVertexData, one row per vertex, checked against the
format’s stride.
For a scene that is built to be released whole (a chapter, a level),
drop_buffer(base, buffer, camera) retires an offscreen buffer and the
camera that drew into it in a safe order, and purge_state_caches() clears
the engine’s render-state caches, which would otherwise keep the released
scene’s textures alive, and runs the garbage collector.
Building meshes¶
GeomBuilder collects triangles from any number of primitives and
bakes them into a single GeomNode with positions, normals and colours, so
a model of a hundred parts is one draw call. The methods return the builder,
so they chain:
import math
from acron.geom import GeomBuilder, color4, xform
hull = color4(0x8a9bb0)
lamp = color4(0xffd27a)
gb = GeomBuilder()
gb.rounded_box(2.0, 4.0, 1.0, 0.2, color=hull)
gb.cylinder(0.1, 0.1, 1.5, segs=8,
xf=xform(pos=(0, 0, 1.2), rotate=((1, 0, 0), math.pi / 2)),
color=hull)
gb.sphere(0.25, xf=xform(pos=(0, 0, 2.0)), color=lamp)
model = base.render.attach_new_node(gb.build("lamp_post"))
The conventions are fixed: boxes are centred on the origin; cylinders and
cones run along +Y, centred; circles, rings and planes lie in the XY plane,
facing +Z; lathe(profile) spins a list of (radius, height) points
about +Y. The other shapes are box, cone, uv_sphere (also partial
spheres), icosphere (with detail subdivisions and an optional
displace function over all the vertices) and octahedron.
xform(pos, rotate, scale) composes a transform for xf – scale, then
the (axis, radians) rotation, then translation. color4(0xRRGGBB)
converts an authored colour to linear. GeomBuilder(uv=True) also writes
texture coordinates.
Quad pools¶
QuadPool keeps max_quads quads in one vertex buffer – particles,
stars, billboards, stamps – with columns you name, written from numpy. Each
column is (name, components), or (name, components, "texcoord") for
the texture coordinates; a position column always comes first.
from acron.quads import QuadPool
pool = QuadPool("sparks", (("aVel", 3), ("aCorner", 2)), 256)
pool.fill_corners(6) # aCorner: after position (3) and aVel (3)
np_ = pool.attach(base.render)
np_.set_shader(spark_shader)
q = pool.quads() # shape (256, 4, stride)
q[:, :, 0:3] = positions[:, None, :]
q[:, :, 3:6] = velocities[:, None, :]
fill_corners(offset) writes the per-corner column, -1..1 pairs, that a
vertex shader offsets each corner by (unit=True writes 0..1, which doubles
as UVs): the engine’s OpenGL context ignores gl_PointSize, so camera-facing
quads are the way to draw point sprites. view() is the buffer one row per
vertex, quads() the same buffer per quad; taking either marks the rows
changed, so take it once in a frame you write. attach(parent) makes the
node never culled, for geometry written in world space.
resize(max_quads) changes the size in place: the node and its vertex
data stay the same objects, so the shader inputs, bin and hide flags set on
the node survive.