# Generate Textures with Local Rules # Book: Cellular Automata From First Principles # Chapter URL: https://programmer.ie/books/cellular-automata-from-first-principles/17-chapter/ # Prose fingerprint (sha256, normalized): d5e6e7c695bd4d93cf76b04c8b161d897607b0cc5babd779331e93687250807d # Snapshot generated at build time by Hugo. This is the complete rendered prose # of the chapter; it is not a transcript of the source file. Cellular automata do not need to represent a literal physical system. They can also be used as visual machines. The same ingredients we have used throughout the book: local state neighborhood perception shared update repetition can generate masks, growth patterns, surface variation and animation. The evaluation question changes. Instead of asking: Is this physically accurate? we ask: Does this local process produce useful, controllable visual structure? That still demands more than “it looks interesting” — and this chapter’s metrics section holds it to that standard. This is also a fork in the procedural-generation tradition worth naming. The dominant lineage builds texture from noise functions — Perlin’s 1985 “An Image Synthesizer” founded it — composing band-limited randomness into shape. The lineage this chapter follows builds texture from iteration: structure emerges from repeated local operations instead. Reaction-diffusion textures (Turk 1991) sit squarely in the second lineage, which makes Chapter 13’s reaction-diffusion a texture chapter in disguise. Start with raw scalar noise import numpy as np rng = np.random.default_rng(42) texture = rng.random( (160, 160) ) Raw noise contains variation but little coherent structure. A local process can create spatial correlation. Smooth and sharpen locally def neighborhood_mean(grid): total = np.zeros_like(grid) for dy in (-1, 0, 1): for dx in (-1, 0, 1): total += np.roll( np.roll( grid, dy, axis=0, ), dx, axis=1, ) return total / 9.0 This is the same 3×3 mean used for terrain smoothing in the previous chapter, renamed for its new job — one operator, two applications, which is exactly the reuse the book wants to make visible. Smoothing: def smooth( grid, amount=0.20, ): return ( (1 - amount) * grid + amount * neighborhood_mean(grid) ) Sharpening: def sharpen( grid, amount=0.15, ): mean = neighborhood_mean(grid) return np.clip( grid + amount * (grid - mean), 0.0, 1.0, ) Now alternate the two: for _ in range(30): texture = smooth(texture) texture = sharpen(texture) (Verified: stays finite with measurable contrast gain; the clip keeps sharpening bounded.) The result is not magic. It is competition between: homogenization and contrast amplification The same competition motif as reaction-diffusion — smoothing versus a difference-creating process — with cheaper ingredients and no chemistry. Convert continuous structure into a mask mask = texture > 0.52 (Observed coverage ≈ 0.41 on the reference seed — a number, not an impression.) The mask can later be interpreted as: corrosion moss cracks damage cloud coverage paint wear biome edge The generator does not need to know the final semantic label. Grow material from sparse seeds growth = ( rng.random((160, 160)) < 0.01 ).astype(np.uint8) Count neighbors and allow growth only near existing material. Then add a small decay probability: def growth_decay_step( grid, rng, grow_p=0.08, decay_p=0.015, ): neighbors = np.zeros_like(grid, dtype=np.uint8) material = (grid == 1).astype(np.uint8) for dy in (-1, 0, 1): for dx in (-1, 0, 1): if dy == 0 and dx == 0: continue neighbors += np.roll( np.roll(material, dy, axis=0), dx, axis=1 ) next_grid = grid.copy() grow = ( (grid == 0) & (neighbors >= 2) & (rng.random(grid.shape) < grow_p) ) decay = ( (grid == 1) & (rng.random(grid.shape) < decay_p) ) next_grid[grow] = 1 next_grid[decay] = 0 return next_grid Now the system contains competing creation and removal processes. The figure compares: raw noise local smooth/sharpen dynamics thresholded mask growth + decay That comparison is more useful than presenting four unrelated pretty images because every panel exposes a different mechanism. The full pipeline behind them: flowchart LR N[noise seed] --> E[evolve: smooth/sharpen or growth/decay] E --> S[structure: mask or material field] S --> M[measure: coverage, contrast, change] M --> R[render / search: keep, reject, refine] Two lineages produce such textures, with different trade-offs: Approach Mechanism Visual effect Limitation Noise-based (Perlin lineage) composed band-limited noise controllable multi-scale detail no local dynamics to interrogate Iterative (this chapter) repeated local rules evolving structure with measurable behavior costlier; needs search + validation Separate hidden state from rendered appearance A richer texture system might store: state = np.zeros( (160, 160, 3), dtype=np.float32, ) with: channel 0 = material channel 1 = moisture channel 2 = damage The internal state can drive local updates. A separate render function can convert it to RGB: def render(state): material = state[..., 0] moisture = state[..., 1] damage = state[..., 2] rgb = np.stack( [ material * (1 - damage), material * ( 1 - 0.5 * damage ), material * ( 1 - moisture ), ], axis=-1, ) return np.clip( rgb, 0.0, 1.0, ) This separation becomes extremely important later. A cell can carry information needed for local computation without every channel having a direct visual interpretation. Chapter 39 will make hidden channels the entire model; this is their first appearance, in a visual setting where the distinction is easy to see. Animate the process, not only the result A final frame may hide the interesting dynamics. Store intermediate states: frames = [] for _ in range(200): frames.append( growth.copy() ) growth = growth_decay_step( growth, rng, ) Now the visual artifact can show: nucleation growth competition decay reorganization rather than only the endpoint. For some chapters later in the book, animation will be more informative than a static PNG. Measure useful visual properties Aesthetic quality is partly subjective. But we can still expose measurable properties. Coverage def coverage(mask): return float(mask.mean()) Mean local contrast def mean_local_contrast(grid): return float( np.mean( np.abs( grid - neighborhood_mean(grid) ) ) ) Temporal change def frame_change(a, b): return float( np.mean( np.abs( a.astype(float) - b.astype(float) ) ) ) Those metrics let us express design constraints such as: coverage near 45% moderate local contrast non-zero but bounded animation rate Search rather than hand-tune forever candidates = [] for seed in range(100): rng = np.random.default_rng(seed) grid = ( rng.random((128, 128)) < 0.01 ).astype(np.uint8) for _ in range(80): grid = growth_decay_step( grid, rng, grow_p=0.08, decay_p=0.015, ) score = abs( coverage(grid) - 0.45 ) candidates.append( (score, seed, grid) ) candidates.sort( key=lambda x: x[0] ) The system is programmable at two levels: local rule -> produces candidate evaluation/search -> chooses candidate That is the bridge into Part III. What the richer worlds taught us We used one local-computation viewpoint to build: Life-like rule families and multi-state rules stochastic spreading forest fire traffic diffusion reaction-diffusion predator-prey dynamics caves terrain textures The semantics changed dramatically. The computational skeleton did not: local state local perception shared transition repeated update measurement We now know how to build cellular worlds. The next question is harder: How do we tell which worlds are dynamically interesting? In Part III we will measure density, activity, spatial variation, entropy, recurrence and sensitivity, then use those measurements to search rule space systematically. Research Turk, G. — Generating Textures on Arbitrary Surfaces Using Reaction-Diffusion (SIGGRAPH ‘91). The proof that this chapter’s lineage is industrially real: reaction-diffusion iteration generating usable surface textures, the direct descendant of Chapter 13’s Gray-Scott patterns. Read it to see where local-iteration textures beat noise functions — and what production use demands. https://faculty.cc.gatech.edu/~turk/my_papers/reaction_diffusion.pdf Perlin, K. — An Image Synthesizer (Computer Graphics 19(3), 1985). The other lineage: texture from composed band-limited noise rather than from iteration. Understand it to know what local-CA textures offer that noise functions do not — and vice versa. The contrast sharpens both. https://doi.org/10.1145/325165.325247