A grass blade from its cell¶
A GRASS BLADE'S TRIANGLE.
- Requires
- the cell's column
ixand rowiz(integers), the cells'spacing(m), the blades' meanheight(m) and itsspread(1), their basewidth(m), their meanleanfrom vertical and itslean_spread(rad), the lawn'sgrain(rad, from +x toward +z) and the share of each blade's lean along it,grain_share(1). - Provides
- (a, b, tip), the base's corners and the tip (m).
- Law
- from the cell's hashes h: the base in the cell's middle half, the height height (1 + spread (2h - 1)), the way a random bearing blended toward the grain's, the tilt lean + lean_spread (2h - 1), the base
widthacross the way, the tipheightup the tilted way. - Source
- Habel, Wimmer and Jeschke, Instant Animated Grass (WSCG 2007).
- Proof
- reference, invariant, limit.
render.blade(args, kwargs)A grass blade's triangle from its cell of the lawn, its base, height, lean and the way it leans each a hash of the cell, so nothing is stored per blade. Part of the table view's grass.
In [1]:
Copied!
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
from lusine import trait_run
F32 = np.float32
U = float(np.finfo(F32).eps) / 2 # binary32's unit roundoff, 2^-24
rng = np.random.default_rng(2026)
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
from lusine import trait_run
F32 = np.float32
U = float(np.finfo(F32).eps) / 2 # binary32's unit roundoff, 2^-24
rng = np.random.default_rng(2026)
The kernel¶
The law as the library states it.
In [2]:
Copied!
print(trait_run.source('render/blade.trait'))
print(trait_run.source('render/blade.trait'))
// lusine.render.blade: a grass blade's triangle, from its cell of the lawn. The ground is cut into
// square cells `spacing` across, one blade a cell, and everything about a blade comes from a hash of its cell
// (lusine.render.noise_corner, the value noise's, salted per property), so nothing is stored per blade and a frame is
// the same bits everywhere:
// - its base: a point in the middle half of its cell, on the ground;
// - its height: height (1 + spread (2 h - 1));
// - the way it leans: the unit horizontal direction blending a random bearing toward the grain's (a mown lawn's
// blades lie with its stripes), grain_share of the way, the random one where the blend cancels;
// - how far: lean + lean_spread (2 h - 1) from vertical;
// - its shape: a triangle `width` wide at its base, across the way it leans, tapering to its tip, height up its lean.
// Source: the per-cell hashed layout of Habel, Wimmer and Jeschke, Instant Animated Grass (WSCG 2007), section 3, with a
// triangle a blade in place of their billboards.
// Checked against a reference in numpy, which has its own tests.
namespace lusine.render;
// a blade: its base's corners and its tip
struct blade_triangle { vector<float<m>, 3> a; vector<float<m>, 3> b; vector<float<m>, 3> tip; };
blade_triangle blade(integer ix, integer iz, float<m> spacing, float<m> height, float spread, float<m> width,
float<rad> lean, float<rad> lean_spread, float<rad> grain, float grain_share)
{
let hx = noise_corner(cx = ix, cy = 0, cz = iz, octave = 101);
let hz = noise_corner(cx = ix, cy = 0, cz = iz, octave = 102);
let hh = noise_corner(cx = ix, cy = 0, cz = iz, octave = 103);
let hb = noise_corner(cx = ix, cy = 0, cz = iz, octave = 104);
let hl = noise_corner(cx = ix, cy = 0, cz = iz, octave = 105);
let base = vector<float<m>, 3>((ix + 0.25 + 0.5 * hx) * spacing, 0.0 m, (iz + 0.25 + 0.5 * hz) * spacing);
let tall = height * (1.0 + spread * (2.0 * hh - 1.0));
let bearing = 6.283185307179586 * hb;
let random = vector<float, 3>(cos(bearing), 0.0, sin(bearing));
let along = vector<float, 3>(cos(grain), 0.0, sin(grain));
let blend = along * grain_share + random * (1.0 - grain_share);
let way = dot(blend, blend) > 0.000001 ? normalize(blend) : random;
let tilt = lean + lean_spread * (2.0 * hl - 1.0);
let side = vector<float, 3>(-way.z, 0.0, way.x) * (0.5 * width);
let tip = base + vector<float<m>, 3>(way.x * sin(tilt) * tall, cos(tilt) * tall, way.z * sin(tilt) * tall);
return blade_triangle { a = base - side, b = base + side, tip = tip };
}
The program¶
The kernel returns a struct, the base's corners and the tip, so the program here binds three small kernels reading its fields over a lattice of cells.
In [3]:
Copied!
from lusine.kernels.render.blade import blade as reference
TEXT = '''namespace probe;
lusine.version version = { { number = 1 } };
struct item { host integer ix; host integer iz; vector<float<m>, 3> a; vector<float<m>, 3> b; vector<float<m>, 3> tip; };
lattice<item, 1> items(spacing = 1 m, boundary = wall);
lusine.render.blade_triangle shaped(integer ix, integer iz) {
return lusine.render.blade(ix = ix, iz = iz, spacing = 0.014 m, height = 0.03 m, spread = 0.3, width = 0.004 m,
lean = 0.26 rad, lean_spread = 0.17 rad, grain = 0.5 rad, grain_share = 0.5);
}
vector<float<m>, 3> corner_a(integer ix, integer iz) { return shaped(ix = ix, iz = iz).a; }
vector<float<m>, 3> corner_b(integer ix, integer iz) { return shaped(ix = ix, iz = iz).b; }
vector<float<m>, 3> corner_tip(integer ix, integer iz) { return shaped(ix = ix, iz = iz).tip; }
void main() { items.a = corner_a(items.ix, items.iz); items.b = corner_b(items.ix, items.iz);
items.tip = corner_tip(items.ix, items.iz); }
'''
print(TEXT)
def blades(ix, iz):
run = trait_run.Program([], TEXT, {'probe.items': [len(ix)]})
run.store('probe.items', 'ix', ix); run.store('probe.items', 'iz', iz)
out = run.steps('probe.main', 1, [('probe.items', 'a'), ('probe.items', 'b'), ('probe.items', 'tip')])
return tuple(np.asarray(x[0]) for x in out)
from lusine.kernels.render.blade import blade as reference
TEXT = '''namespace probe;
lusine.version version = { { number = 1 } };
struct item { host integer ix; host integer iz; vector<float<m>, 3> a; vector<float<m>, 3> b; vector<float<m>, 3> tip; };
lattice<item, 1> items(spacing = 1 m, boundary = wall);
lusine.render.blade_triangle shaped(integer ix, integer iz) {
return lusine.render.blade(ix = ix, iz = iz, spacing = 0.014 m, height = 0.03 m, spread = 0.3, width = 0.004 m,
lean = 0.26 rad, lean_spread = 0.17 rad, grain = 0.5 rad, grain_share = 0.5);
}
vector<float<m>, 3> corner_a(integer ix, integer iz) { return shaped(ix = ix, iz = iz).a; }
vector<float<m>, 3> corner_b(integer ix, integer iz) { return shaped(ix = ix, iz = iz).b; }
vector<float<m>, 3> corner_tip(integer ix, integer iz) { return shaped(ix = ix, iz = iz).tip; }
void main() { items.a = corner_a(items.ix, items.iz); items.b = corner_b(items.ix, items.iz);
items.tip = corner_tip(items.ix, items.iz); }
'''
print(TEXT)
def blades(ix, iz):
run = trait_run.Program([], TEXT, {'probe.items': [len(ix)]})
run.store('probe.items', 'ix', ix); run.store('probe.items', 'iz', iz)
out = run.steps('probe.main', 1, [('probe.items', 'a'), ('probe.items', 'b'), ('probe.items', 'tip')])
return tuple(np.asarray(x[0]) for x in out)
namespace probe;
lusine.version version = { { number = 1 } };
struct item { host integer ix; host integer iz; vector<float<m>, 3> a; vector<float<m>, 3> b; vector<float<m>, 3> tip; };
lattice<item, 1> items(spacing = 1 m, boundary = wall);
lusine.render.blade_triangle shaped(integer ix, integer iz) {
return lusine.render.blade(ix = ix, iz = iz, spacing = 0.014 m, height = 0.03 m, spread = 0.3, width = 0.004 m,
lean = 0.26 rad, lean_spread = 0.17 rad, grain = 0.5 rad, grain_share = 0.5);
}
vector<float<m>, 3> corner_a(integer ix, integer iz) { return shaped(ix = ix, iz = iz).a; }
vector<float<m>, 3> corner_b(integer ix, integer iz) { return shaped(ix = ix, iz = iz).b; }
vector<float<m>, 3> corner_tip(integer ix, integer iz) { return shaped(ix = ix, iz = iz).tip; }
void main() { items.a = corner_a(items.ix, items.iz); items.b = corner_b(items.ix, items.iz);
items.tip = corner_tip(items.ix, items.iz); }
Reference: a patch of lawn¶
The blades of 400 cells: each corner within binary32's roundings of the reference's (the hash the same bits, the geometry rounded).
In [4]:
Copied!
ix = rng.integers(-500, 500, 400); iz = rng.integers(-500, 500, 400)
a, b, tip = blades(ix, iz)
ra, rb, rt = reference(ix, iz, 0.014, 0.03, 0.3, 0.004, 0.26, 0.17, 0.5, 0.5)
scale = 0.014 * np.maximum(np.abs(ix), np.abs(iz)).max() + 0.03
err = max(np.max(np.abs(x - y)) for x, y in ((a, ra), (b, rb), (tip, rt))) / scale
assert err <= 8 * U, err / U
print(f'every corner within {err / U:.1f} x 2^-24 of the lawn’s extent')
ix = rng.integers(-500, 500, 400); iz = rng.integers(-500, 500, 400)
a, b, tip = blades(ix, iz)
ra, rb, rt = reference(ix, iz, 0.014, 0.03, 0.3, 0.004, 0.26, 0.17, 0.5, 0.5)
scale = 0.014 * np.maximum(np.abs(ix), np.abs(iz)).max() + 0.03
err = max(np.max(np.abs(x - y)) for x, y in ((a, ra), (b, rb), (tip, rt))) / scale
assert err <= 8 * U, err / U
print(f'every corner within {err / U:.1f} x 2^-24 of the lawn’s extent')
every corner within 0.0 x 2^-24 of the lawn’s extent
Invariant: in its cell, its size¶
Every base is on the ground in its cell's middle half and the width across; every blade is within height x (1 +- spread).
In [5]:
Copied!
base = (a + b) / 2
cell = np.stack([ix, iz], axis=1) * 0.014
assert np.all(base[:, [0, 2]] >= cell + 0.0035 - 1e-6) and np.all(base[:, [0, 2]] <= cell + 0.0105 + 1e-6)
assert np.allclose(np.linalg.norm(b - a, axis=1), 0.004, atol=1e-6)
length = np.linalg.norm(tip - base, axis=1)
assert np.all((length >= 0.021 - 1e-6) & (length <= 0.039 + 1e-6))
print('every base in its cell, the width across; every blade within height x (1 +- spread)')
base = (a + b) / 2
cell = np.stack([ix, iz], axis=1) * 0.014
assert np.all(base[:, [0, 2]] >= cell + 0.0035 - 1e-6) and np.all(base[:, [0, 2]] <= cell + 0.0105 + 1e-6)
assert np.allclose(np.linalg.norm(b - a, axis=1), 0.004, atol=1e-6)
length = np.linalg.norm(tip - base, axis=1)
assert np.all((length >= 0.021 - 1e-6) & (length <= 0.039 + 1e-6))
print('every base in its cell, the width across; every blade within height x (1 +- spread)')
every base in its cell, the width across; every blade within height x (1 +- spread)
Limit: the same blade every time¶
A cell's blade is its hash's: two runs over the same cells give the same bits.
In [6]:
Copied!
again = blades(ix, iz)
assert all(np.array_equal(x, y) for x, y in zip((a, b, tip), again))
print('the same bits twice')
again = blades(ix, iz)
assert all(np.array_equal(x, y) for x, y in zip((a, b, tip), again))
print('the same bits twice')
the same bits twice