Skip to content
Chromatic Coherence

Hue draws · Engine · Recipes

A world is made of things you can ask for by name.

The engine ships a world's worth of assets — animals, birds, trees, water and the life in it — and none of them is a model file. Each one is a set of proportions and a real size in metres, grown on your machine. Pick one and read the code that grew it.

Edition

Shadows

Objective

How many

Stage

Roe deer

ROE_DEER

Grown from thirty-odd proportions and coated with a counter-shaded pelt, then run on its own ethology — it grazes, lifts its head to scan, and lowers it again more slowly than it raised it. Every one of those timings is the species', not the scene's.

Tier

Animal triangles

Ground vertices

Frame rate

GPU time

Withers

0.67 m · declared

The code that grew it

recQuadruped · the exact code that grew this scene

import { ROE_DEER, heightfield, norm, quadrupedExtent, v3, GOLDEN_ANGLE, GOLDEN_FRACTION, attachObjectives, behaveQuadruped, growAnimal, herdCell, quadrupedAgent } from "@hue/engine";
import type { BipedSpec, QuadrupedSpec, World, Animal, QuadrupedAgent, QuadrupedNeighbour } from "@hue/engine";

const UPM = 10;

/** A metre, in world units. Used for the ground and the camera — never for the animal itself. */
const M = UPM;

const STAGE_YAW = 0.72;

function framing(specs: readonly QuadrupedSpec[]) {
  const y = yardstickAll(specs);
  
  const { widen } = shareStageOf(specs, 0);
  return { target: v3(0, y.top * 0.62, 0), dist: y.reach * 4.6 * widen, pitch: 0.2, yaw: STAGE_YAW };
}

let REF: { reach: number; top: number } | null = null;

function yardstick(spec: QuadrupedSpec) {
  const ref = (REF ??= (() => {
    const e = quadrupedExtent(ROE_DEER);
    return { reach: Math.max(e.lengthH, e.heightH), top: e.heightH };
  })());
  const h = spec.withersM * M;
  const e = quadrupedExtent(spec);
  return {
    reach: (h * Math.max(e.lengthH, e.heightH)) / ref.reach,
    top: (h * e.heightH) / ref.top,
    
    span: h * Math.max(e.lengthH, e.heightH),
  };
}

function framingBiped(specs: readonly BipedSpec[]) {
  const top = bipedTop(specs);
  const { widen } = shareStage(bipedSpan(specs), specs.length, 0);
  return { target: v3(0, top * 0.55, 0), dist: top * 2.6 * widen, pitch: 0.12, yaw: STAGE_YAW };
}

function bipedTop(specs: readonly BipedSpec[]): number {
  return Math.max(...specs.map((s) => s.heightM)) * M;
}

const RING_IN = 0.6, RING_OUT = 1.5;

function bipedSpan(specs: readonly BipedSpec[]): number {
  return (RING_IN + RING_OUT) * 2 * M + Math.max(...specs.map((s) => s.heightM)) * M * 0.3;
}

function yardstickAll(specs: readonly QuadrupedSpec[]) {
  const ys = specs.map(yardstick);
  return {
    reach: Math.max(...ys.map((y) => y.reach)),
    top: Math.max(...ys.map((y) => y.top)),
    span: Math.max(...ys.map((y) => y.span)),
  };
}

function shareStage(span: number, count: number, spread: number) {
  if (count < 2) return { ring: 0, widen: 1 };
  const foot = spread + span;
  const ring = foot / Math.sin(Math.PI / count);
  /* the half-width the eye must hold, against the half-width one group alone presents */
  const solo = spread + span / 2;
  return { ring, widen: (ring + solo) / solo };
}

function shareStageOf(specs: readonly QuadrupedSpec[], spread: number) {
  return shareStage(yardstickAll(specs).span, specs.length, spread);
}

const groundY = (x: number, z: number) =>
  Math.sin(x * 0.018) * Math.cos(z * 0.021) * 0.06 * M +
  Math.sin(x * 0.041 + 1.3) * 0.03 * M;

const GROUND_M = 26;

function groundSpanFor(distU: number): number {
  return (distU / M) * 6;
}

function stage(w: World, spanM = GROUND_M) {
  w.unitsPerMetre = UPM;

  const span = Math.max(GROUND_M, spanM);
  /* one row per ~0.46 m, which is what 56 rows over 26 m already was — held so a larger stage is
     not a coarser one */
  const rows = Math.max(56, Math.round(56 * (span / GROUND_M)));
  w.mesh(heightfield(groundY, span * M, span * M, rows, rows), {
    hue: 96, sat: 16, light: 17, gloss: 0.04,
  });

  // A single key, high and to the side. lights[0] is the one that casts the shadow map, and a
  // shadow on the ground is most of what makes a standing animal look like it stands on something.
  w.light.signed = false;
  w.light.dir = norm(v3(-0.42, 1, 0.38));
  w.light.ambient = 0.44;
  w.light.diffuse = 0.62;
  w.specular = 0.3;
  w.lights.push({ p: v3(-9 * M, 14 * M, 7 * M), intensity: 1.15, falloff: 40 * M });
  w.lights.push({ p: v3(11 * M, 5 * M, -8 * M), intensity: 0.34, falloff: 34 * M });

  w.filmic = true;
  w.grade = true;
  w.vignette = 0.42;
  w.ambientSky = v3(0.84, 0.88, 1.08);
  w.ambientGround = v3(1.04, 0.96, 0.84);
}

function framingBird(specs: readonly BirdFramable[], circuitU: number) {
  const top = birdTop(specs);
  const reach = birdReach(specs);
  const { widen } = shareStage(circuitU * 2 + reach, specs.length, 0);
  
  return {
    target: v3(0, top * 0.7, 0),
    dist: Math.max(reach * 4.2, circuitU * 2.1) * widen,
    pitch: 0.16, yaw: STAGE_YAW,
  };
}

function birdTop(specs: readonly BirdFramable[]): number {
  return Math.max(...specs.map((s) => s.lengthM)) * M * 0.5;
}

function birdReach(specs: readonly BirdFramable[]): number {
  return Math.max(...specs.map((s) => Math.max(s.lengthM, s.wingspanM))) * M;
}

type BirdFramable = { readonly lengthM: number; readonly wingspanM: number };

function framingTree(specs: readonly TreeFramable[], count: number) {
  const tallest = Math.max(...specs.map((s) => s.heightM[1])) * M;
  const { widen } = shareStage(treeSpan(specs, count), specs.length, 0);
  return {
    target: v3(0, tallest * 0.45, 0),
    dist: tallest * 1.9 * widen,
    pitch: 0.14, yaw: STAGE_YAW,
  };
}

function treeSpan(specs: readonly TreeFramable[], count: number): number {
  const tallest = Math.max(...specs.map((s) => s.heightM[1])) * M;
  return tallest * 0.55 * Math.max(1, Math.sqrt(count));
}

type TreeFramable = { readonly heightM: readonly [number, number] };

/** A roe deer grazes ~18 s between head-lifts. Real, and a photograph on a bench. */
const WATCH = 4;

const HERD = 7;

const HERD_MIN = 5;

const benchClock = (spec: QuadrupedSpec): QuadrupedSpec => ({
  ...spec,
  name: `${spec.name}-bench`,
  ethology: {
    ...spec.ethology,
    boutS: spec.ethology.boutS * 0.06,
    moveShare: 0.9,
    rangeBodies: Math.min(5, spec.ethology.rangeBodies),
  },
});

export const recQuadruped: AssetRecipe = (w, assets, view) => {
  
  const herd: QuadrupedAsset[] = [];
  for (const a of assets) {
    if (!isQuadrupedAsset(a)) {
      throw new Error(
        `recQuadruped: given an asset that is not an animal — it has no \`pelage\`. This recipe ` +
        `grows quadrupeds; \`recBiped\` grows people.`);
    }
    herd.push(a);
  }

  /* C walks, so it gets the bench clock and a gait; A and B are the animal exactly as shipped. */
  const walking = view.objective === "c";
  const specs = herd.map((a) => (walking ? benchClock(a.spec) : a.spec));

  
  const cam = framing(specs);
  stage(w, groundSpanFor(cam.dist));

  
  const shares = specs.length > 1;
  if (view.shared !== shares) {
    throw new Error(
      `recQuadruped: view.shared is ${view.shared} and ${specs.length} asset(s) were given. ` +
      `A shared stage is more than one asset and one asset is not a shared stage — pass the ` +
      `assets the reader asked for, or clear the flag.`);
  }

  
  const cell = shares ? herdCell(w, specs) : 0;

  
  const per = view.many ? Math.max(HERD_MIN, Math.round(HERD * w.tier.detail)) : 1;
  /* how far one group's outermost animal reaches from its own centre, in world units */
  const spread = per === 1 ? 0 : (RING_IN + RING_OUT) * M;
  
  const { ring, widen } = shareStageOf(specs, spread);

  const animals: Animal[] = [];
  // where each one was minted, and WHICH SPECIES it is. C needs both again to give every agent its
  // own home disc and its own behaviour machine, and recomputing the spiral there would be two
  // copies of one placement.
  const spots: { x: number; z: number; facing: number; spec: QuadrupedSpec }[] = [];
  for (let g = 0; g < specs.length; g++) {
    const spec = specs[g]!;
    
    const ga = GOLDEN_ANGLE + (g * 2 * Math.PI) / specs.length;
    const gx = Math.cos(ga) * ring, gz = Math.sin(ga) * ring;
    for (let i = 0; i < per; i++) {
      // a golden-angle spiral: any count spreads evenly, and no two animals line up
      const a = i * GOLDEN_ANGLE, r = per === 1 ? 0 : (RING_IN + RING_OUT * Math.sqrt(i / per)) * M;
      const x = gx + Math.cos(a) * r, z = gz + Math.sin(a) * r;
      spots.push({ x, z, facing: -0.42 + a * 0.3, spec });
      animals.push(growAnimal(w, spec, {
        coat: herd[g]!.pelage,
        body: "marched",
        at: v3(x, 0, z),
        facing: -0.42 + a * 0.3,
        groundY,
        cell,
        animate: view.objective === "a" ? { rate: WATCH } : false,
        // C is the only one that travels, and legs cost four more baked poses and four more batches
        gait: walking,
        castShadow: specs.length * per === 1,
      }));
    }
  }

  /**
   * THE OBJECTIVES LAYER — a goal, and the engine drives motion toward it.
   *
   * Each objective is a camera goal plus, where it needs one, a channel that carries the herd's
   * pose — so the same toggle moves the eye AND the animals. `pace` is seconds to ~95% of the way.
   */
  const obj = attachObjectives(w);
  
  const h = Math.max(...specs.map((s) => s.withersM)) * M;
  
  const { reach, top } = yardstickAll(specs);

  if (view.objective === "a") {
    // A — GRAZING: in close, low, and the animals keep their own clock
    obj.define("watch", {
      
      camera: { target: v3(0, top * 0.62, 0), dist: reach * (view.many ? 9 : 4.6) * widen, pitch: 0.2 },
      pace: 2.2,
    });
  } else if (view.objective === "b") {
    obj.define("watch", {
      // B — ALERT: the eye pulls back and up, and the heads come up in a ripple, not together
      camera: { target: v3(0, top * 0.9, 0), dist: reach * (view.many ? 14 : 7.4) * widen, pitch: 0.34 },
      pace: 2.2,
      
      custom: (progress) => {
        for (let i = 0; i < animals.length; i++) {
          const lag = animals.length === 1 ? 0 : ((i * GOLDEN_FRACTION) % 1) * 0.55;
          animals[i]!.setPose((progress - lag) / (1 - lag));
        }
      },
    });
  } else if (view.objective === "c") {
    
    
    
    const reachU = Math.max(...specs.map((s, g) =>
      s.ethology.rangeBodies * (animals[g * per]?.gait?.bodyM ?? 1) * M));
    
    const spreadU = spread + ring;
    obj.define("watch", {
      camera: { target: v3(0, h * 0.7, 0), dist: (reachU + spreadU) * 2.2, pitch: 0.3 },
      pace: 2.2,
    });

    
    const agents: QuadrupedAgent[] = spots.map((s, i) =>
      quadrupedAgent(s.spec, 4200 + i * 97, { id: i, x: s.x, z: s.z, facing: s.facing }));

    
    const FOLLOW = 2.5;
    let since = FOLLOW;

    
    const near: QuadrupedNeighbour[] = spots.map((s) => ({
      x: s.x, z: s.z,
      r: (quadrupedExtent(s.spec).widthH * s.spec.withersM * M) / 2,
    }));

    w.grow((_t, dt) => {
      
      for (let i = 0; i < agents.length; i++) {
        near[i]!.x = agents[i]!.x;
        near[i]!.z = agents[i]!.z;
      }
      let cx = 0, cz = 0;
      for (let i = 0; i < agents.length; i++) {
        // `behaveQuadruped` is a delta integrator — it takes `dt`, never the elapsed clock
        const a = behaveQuadruped(spots[i]!.spec, agents[i]!, dt, w, near);
        animals[i]!.moveTo(v3(a.x, 0, a.z), a.facing);
        /* `swing > 0` is the machine's own moving test, for amble/walk/trot/flee and nothing else.
           Re-deriving one from speed risks a different answer at the boundary. */
        if (a.swing > 0) animals[i]!.setGait(a.gaitPhase);
        else { animals[i]!.setGait(null); animals[i]!.setPose(a.morph); }
        cx += a.x; cz += a.z;
      }
      since += dt;
      if (since >= FOLLOW) {
        since = 0;
        obj.retarget("watch", { camera: { target: v3(cx / agents.length, h * 0.7, cz / agents.length) } });
      }
    });
  } else {
    
    const exhaustive: never = view.objective;
    throw new Error(`recQuadruped: no objective "${String(exhaustive)}".`);
  }
};

WHY A CATALOGUE

Nothing here is a file you download.

A species is data — thirty proportions, a real size in metres and a behaviour clock — and one builder reads it. That is why a second animal is a row rather than a rewrite, and why none of this needs an asset pipeline before you can see anything.

AND WHAT IT COSTS

Every triangle on this page was grown in the browser you are reading it in, by a package with no dependencies and no asset pipeline to run first.