Araucaria
The pewén, Araucaria araucana, grows in the Andes and the Nahuelbuta range of south-central Chile and in Argentina. It is a good first tree for a generator: its branches come out in whorls and barely fork, and age alone turns a pyramid into an umbrella. Move Age, drag the tree to turn it, and switch between lines and pixel art: it is the same plant, from the same seed. How it works.
El pewén, Araucaria araucana, crece en los Andes y en la cordillera de Nahuelbuta, en el centro-sur de Chile, y en Argentina. Es un buen primer árbol para un generador: sus ramas salen en verticilos y casi no se ramifican, y basta la edad para convertir una pirámide en un paraguas. Mueve la Edad, arrastra el árbol para girarlo y cambia entre líneas y pixel art: es la misma planta, de la misma semilla. Cómo funciona.
The idea in plain words
The generator does not draw a tree. It writes down what the tree is made of: a trunk, whorls of branches, a few branchlets, thousands of scale leaves and, on adult trees, cones. Each piece has a kind, a position in 3D, a radius and a colour name. Drawing is someone else's job, and there are two drawers on this page: one in lines and shapes, one in pixel art.
Think of a recipe and two cooks. The recipe says "a column, then rings of branches"; one cook pipes it in icing, the other builds it in Lego. The analogy has limits: a recipe leaves room for each cook's taste, while here the description fixes every branch, and the drawers only decide how much of it to show.
The tree itself is a good first subject because its rules are few and visible. Branches leave the trunk in whorls, several at the same height, and barely fork. Every branch is covered in stiff, sharp, triangular scale leaves, so a branch looks like a green rope. The shape changes with age: young trees are pyramids with whorls down to the ground; old ones lose their lower branches and keep a bare column with an umbrella of a crown on top. Slide Age from 0 to 100 % and watch both drawers follow.
Why it matters
The pewén is one of the trees of the Andes of south-central Chile and of the Nahuelbuta range, and a living link to a very old family. Araucariaceae pollen goes back to the Triassic; in South America and Antarctica the oldest reliable macrofossils are from the Early Jurassic, and the family was most widespread in the Early Cretaceous, when it reached as far north as Colombia and Brazil (Panti and colleagues, 2012). The petrified forest of Cerro Cuadrado, in Argentine Patagonia, holds cones of Araucaria mirabilis from the Middle to Late Jurassic. "Living fossil" is a popular label, not a technical term: what is old is the lineage, not the species.
The trees are usually dioecious: some are male, with elongated pollen cones, and some female, with globose seed cones that mature about 18 months after pollination according to Wikipedia, or in 2–3 years according to the Gymnosperm Database. The seeds, piñones or ngulliw in Mapudungun, are food: the Pehuenche, "the people of the pewén", take their name from it, and the Gymnosperm Database reports that in some Mapuche communities the seeds make up 10–15 % of the diet in the harvest season. The bark can be 15 cm thick, which may be an adaptation to fire. Chile declared the species a natural monument in 1990 (Decree 43 of the Ministry of Agriculture), which bans felling it, and the IUCN has listed it as Endangered since 2013. Use the Sex buttons to switch cone types; trees below 45 % age have none.
For a game, the point of the split is reuse. The same description can feed the line drawing you see here, a 16-pixel sprite for a map, a 32-pixel one for a closer view and, later, a 3D mesh, all from one seed, so a forest stays the same forest whatever the camera.
Is there a theorem?
No theorem decides how an araucaria grows. What exists is measurement, and one result matters here. It is tempting to read a tree's age from its whorls, one per year. Lusk and Le-Quesne (2000) checked this on young araucarias in the coastal range and found it false: every one of their 35 trees had more growth rings than whorls, with a whorl about every 2.1 years in the sun and every 2.6 in the shade. That is why Age on this page is a life stage from 0 to 100 %, not a number of years, and why whorl spacing is its own slider.
On the generative side, the classic reference is Prusinkiewicz and Lindenmayer's The Algorithmic Beauty of Plants (1990), which builds plants from rewriting rules (L-systems). This page uses something simpler: an explicit loop over whorls and branches, because the araucaria's structure is regular enough not to need a grammar.
The equations
Heights in metres. H is the mature height, a the age in [0, 1], s(·) the smoothstep curve from 0 to 1.
- Current height: h = H · (0.12 + 0.88 · s(a / 0.75))
- Bare share of the trunk: b = 0.8 · s((a − 0.25) / 0.6)
- Branch length at a depth d = h − y below the apex: L(d) = min(W, 0.3 + d / tan p), with W the crown width (longest branch)
- Crown pitch: p = 65° + (P − 65°) · s((a − 0.35) / 0.6), with P the pitch of the old crown
- Branch elevation along the branch, t from 0 to 1: θ(t) = angle − droop + curve · t²
- Scale leaves around a branch: φk = k · 137.5°
Read the branch length in words: seen from the side, the crown is a triangle. Branches grow in a straight line with their depth below the apex, at the slope p, until they reach the crown width W; below that they are all long. The young tree has a steep pitch, a pyramid; with age the pitch falls to P, and the lower crown is gone (the bare share). What is left is a low, wide triangle on a bare column: the umbrella.
That triangle is the young crown and the Triangle option. The old crown follows a profile instead (Heart by default, each shape with its own two sliders; Show the guide figure draws it over the tree): with age, each branch is pulled towards the length of a flattened oval (Disc) or of two ovals, a parasol and a sparser hanging disc below (Two discs), a flattened cardioid whose low branches hang into two lobes (Heart) or a concave disc whose tips curl up, with a lobe below (Bowl); its droop changes with it. Bouquets gather the foliage at the tips: the inner stretch of the branch goes almost bare and the last third opens into a cone of branchlets of uneven lengths. ✦ IA
What the picture does and does not say
- The age curves are a model, not a growth table. They reproduce the known shapes (pyramid, then umbrella) and heights (30–40 m), not measured growth rates; the crown proportions, about twice as wide as tall at 100 %, are set by eye.
- The tree is to scale (the grey figure is 1.7 m), but the organs are not: scale leaves are drawn about four times life size, cones about twice, or they would vanish at this zoom.
- Branches per whorl: one source gives 5 to 7, and I found no primary measurement. The slider goes from 3 to 9.
- Branchlets are simplified to pairs on the outer part of each branch, and cones appear above 45 % age as a rule of the model, not a documented age of first coning.
- In pixel art the tree fills the sprite whatever its age, so a sapling and an old tree are not to scale with each other. Wind only moves the line drawing.
TODO: A limits bullet for the stylized sprites: they exaggerate on purpose (side-on arms, tufts at the tips, trunk squashed to 45 %, crown up to 1.6 times wider than tall), so they trade realism for a readable silhouette. Say it is an art choice, and link the art-direction idea.
How it works
This part walks through the code of the scene, step by step. It covers:
- A plant description that every renderer can read.
- Age as a handful of curves.
- Whorls and branch lengths.
- Branches as polylines, and scale leaves in a golden-angle spiral.
- A line renderer with depth and wind.
- A pixel renderer with its own level of detail.
- A stylized pixel renderer that keeps the arms readable.
- Palettes: fixed ones and your own, generated in OKLCH.
- Exporting a sprite sheet.
Plain JavaScript and the Canvas 2D API, no libraries. The generator and both renderers are about 470 lines and touch no DOM, so they run in Node for tests and timing; the page script is about 220.
TODO: Update the line counts (stylized renderer, palette.js and palette-picker.js are new).
1. A description, not a drawing
The plant is two flat lists. Parts are the woody pieces, with their two ends in 3D and a radius at each; details are leaves and cones, with a position, a direction and a size. Each entry points to its parent by index instead of nesting, so the lists are easy to loop over, to serialise as JSON and, later, to port to C# as arrays of structs. Colours are names ("bark", "leaf"); each renderer has its own palette.
// A part is anything with volume along an axis: a trunk piece, a branch, a bone.
// a, b: ends in metres (y up); r0, r1: radius at each end. parent: index of the part it
// hangs from, -1 for a root. group: the whorl. role: a colour name, not a hex.
export function addPart(art, p) {
art.parts.push({ parent: -1, group: -1, tone: 0.5, ...p });
return art.parts.length - 1;
}
// A detail is small and countless: a scale leaf, a cone. shape tells any renderer how to
// draw it without knowing the species: "tick" (a short line along dir) or "disc".
export function addDetail(art, d) {
art.details.push({ part: -1, tone: 0.5, shape: "disc", ...d });
return art.details.length - 1;
} Positions are 3D from the start, with the trunk on the y axis. The side view is just a projection: drag the tree and it turns, because the renderers rotate the points around the trunk before dropping the depth.
2. Age as curves
Everything that changes with age comes from one small function: the current height, how much of the trunk is bare, how far branches reach and how steep the crown is. The smoothstep curves make every change gradual, so the slider never jumps.
export function stage(p) {
const h = p.height * (0.12 + 0.88 * smoothstep(0, 0.75, p.age)); // current height
const bare = 0.8 * smoothstep(0.25, 0.85, p.age); // bare share of the trunk
const reach = p.crownWidth; // longest branch
const pitch = 65 + (p.crownPitch - 65) * smoothstep(0.35, 0.95, p.age); // a steep pyramid flattens into an umbrella
return { h, bare, reach, pitch, cones: p.age >= 0.45 };
} 3. Whorls
Whorls are placed from the apex down, at the chosen spacing with a little jitter, until they reach the bare part of the trunk. Each whorl gets a random rotation, and each branch a small angular offset, a length variation and a chance to be missing, higher at the bottom of an old crown. All the randomness comes from the hash of Islands Lab (seed, index, salt), so a branch keeps its shape when you change something unrelated.
const crownBottom = st.bare * st.h;
const count = Math.floor((st.h - 0.3) / p.spacing);
for (let i = 0; i < count; i++) {
const y = st.h - 0.3 - (i + (u01(s, i, SALT.whorl) - 0.5) * 0.3) * p.spacing;
if (y < Math.max(0.15, crownBottom)) break;
const d = st.h - y; // distance below the apex
const length = Math.min(st.reach, 0.3 + d / Math.tan(st.pitch * RAD));
const crownT = (y - crownBottom) / Math.max(1e-6, st.h - crownBottom); // 0 at the crown base, 1 at the top
const droop = p.age * 35 * (1 - crownT); // lower crown branches hang lower in old trees
const offset = u01(s, i, SALT.az) * 2 * Math.PI;
…
for (let j = 0; j < p.perWhorl; j++) {
const id = i * 16 + j;
// Old trees lose branches at the bottom of the crown.
if (u01(s, id, SALT.miss) < 0.04 + 0.35 * p.age * (1 - crownT)) continue;
const az = offset + ((j + (u01(s, id, SALT.ang) - 0.5) * 0.3) / p.perWhorl) * 2 * Math.PI;
const L = length * (0.85 + 0.3 * u01(s, id, SALT.len));
…
}
} Move Whorl spacing and the tree keeps its outline but changes its count of tiers.
4. Branches and scale leaves
A branch is eight segments. Its elevation starts at the branch angle, minus the droop of the lower crown, and turns up towards the tip with the square of the distance: the slight upturn of araucaria branches.
for (let k = 0; k < n; k++) {
const t = (k + 0.5) / n;
const elev = (p.angle - b.droop + p.curve * t * t) * RAD;
const next = add3(pos, [dirH[0] * Math.cos(elev) * step, Math.sin(elev) * step, dirH[2] * Math.cos(elev) * step]);
…
parent = addPart(plant, { kind: "branch", a: pos, b: next, r0: ra, r1: rb, role: "bark", group: b.whorl, parent, tone });
scales(plant, p, pos, next, ra, parent, b.whorl, b.id * 64 + k);
…
pos = next;
} Scale leaves wrap each segment in a spiral, each one 137.5° round from the last: the golden angle, which never lines leaves up in rows. That is the same golden ratio as the R1 sequence in Islands Lab part 5. Each leaf points outwards and forwards along the branch.
for (let k = 0; k < count; k++) {
const t = (k + 0.5) / count, ang = (key * 7 + k) * GOLDEN;
const out = add3(mul3(u, Math.cos(ang)), mul3(v, Math.sin(ang)));
const pos = add3([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t], mul3(out, r));
addDetail(plant, { kind: "scale", shape: "tick", p: pos, dir: norm3(add3(out, mul3(axis, 0.8))), size: 0.04, role: "leaf",
part: segment, tone: u01(p.seed, key * 97 + k, SALT.tone) });
} Are real araucaria leaves on a golden-angle spiral?
The sources say the leaves are spirally arranged, not at what angle. The golden angle is a common choice in generators because it spreads leaves evenly; here it is an assumption.
5. Lines and shapes
The line renderer projects every segment and organ, sorts them into six depth buckets and draws the buckets from back to front, fading the far ones towards the background. Inside a bucket, all scale ticks of one tone go into a single path, which keeps about 10,000 leaves at an interactive frame rate.
// Depth buckets, back to front, so the near side covers the far side but strokes of one
// colour can still be batched inside a bucket.
const BUCKETS = 6;
const bucketOf = (depth) => Math.max(0, Math.min(BUCKETS - 1, Math.floor(((depth / reach) * 0.5 + 0.5) * BUCKETS)));
…
const tick = Math.max(2.2 * dpr, 0.04 * view.scale * 4); // scales drawn about 4× life size, never under 2 px
for (const o of plant.details) {
const a = toScreen(o.p, detailGroup(plant, o));
const bk = buckets[bucketOf(a.depth)];
if (o.kind === "scale") {
const d = project(o.dir, view.yaw);
bk.leaf[Math.min(2, Math.floor(o.tone * 3))].push([a.x, a.y, a.x + d.x * tick, a.y - d.y * tick]);
} else {
bk.cones.push([a, o]);
}
} Wind is not in the description. The renderer moves each point when it draws it: the trunk bends with height squared, branches flutter with their distance from the trunk, with two sines, as in Hillside. A future renderer can ignore it or do it on the GPU.
export function windOffset(p, whorl, height, time, strength) {
if (!strength) return [0, 0];
const h = p[1] / height;
const trunk = strength * 0.012 * height * h * h * (Math.sin(time * 0.9) * 0.7 + Math.sin(time * 2.1 + 1.3) * 0.3);
const r = Math.hypot(p[0], p[2]);
const flutter = whorl < 0 ? 0 : strength * 0.04 * r * Math.sin(time * 2.6 + whorl * 0.9 + r * 0.6);
return [trunk + flutter * 0.4, flutter];
} 6. Pixel art has its own level of detail
Shrinking the line drawing to 16 pixels gives grey mush: thousands of sub-pixel strokes averaged together. The pixel renderer reads the same description but decides differently. It fits the tree to the sprite, draws every branch as a line at least one pixel wide with Bresenham's algorithm (1965), and lets the whorls merge into a mass where they are closer than a pixel. Branches on the far side go to their own layer, so they can be darker.
// Branches first, as thin lines: at these sizes a branch and its scales are one pixel
// wide, and whorls closer than a pixel merge into a solid crown on their own.
const leafR = 0.12; // branch radius plus scale length, roughly, in metres
for (const s of plant.parts) {
if (s.kind === "trunk") continue;
const a = project(s.a, yaw), c = project(s.b, yaw);
const v = (a.depth + c.depth) / 2 < -0.15 * reachX ? BACK : FRONT;
line(px(a.x), py(a.y), px(c.x), py(c.y), Math.max(1, Math.round(2 * (s.r0 + leafR) * k)), (x, y) => put(x, y, v));
}
…
// Close one-pixel gaps in the crown, across and down, so small sprites read as a mass.
closeGaps(layer, size); Then each pixel gets one of eight colours, with no blending: lit where the pixel above is empty, in shadow where the one below is, the far side always in shadow, the trunk split in two tones when it is wide enough. A few cones become single pixels, and an outline goes round the outside.
const v = at(x, y);
if (!v) continue;
let c;
if (v === TRUNK) c = (x >= cx && trunkWidth(layer, size, y) > 1) || at(x, y - 1) === FRONT ? BARK_D : BARK;
else if (v === BACK) c = LEAF_D;
else if (!at(x, y - 1)) c = LEAF_L;
else if (!at(x, y + 1)) c = LEAF_D;
else c = LEAF;
data[y * size + x] = c; Switch to 16 px and slide Age: the sapling is a stack of tiers, the adult a cone, the old tree a slab on a stick, which is about what an araucaria can be at that size.
TODO: Since 2026-10-29 the page draws only the Clumps sprite (render-pixel-silhouette.js: the guide figure as a mask, one clump per branch tip, chosen at the gate). Sections 6 and 6b describe its two predecessors, the faithful and the stylized sprites: present them as the steps that led to it, and add a section on the clumps (silhouette first, exaggerate the trait, clumps; see research/2026-10-29-pixel-art-tecnicas.md).
6b. Aesthetics over accuracy: the stylized sprite
TODO: Why the faithful sprite loses the arms at 16 and 32 px, and the rule: keep the most significant trait. How: tiers chosen by pixel gap, every arm drawn side-on, only the outermost arms at 16 px, a tuft (ellipse) at each tip and mid-arm on long arms at 32 px, bare trunk squashed to 45 %, horizontal stretch up to 1.6, apex cap and outline. Snippet from render-pixel-stylized.js. Before/after image at 16 and 32 px.
6c. Palettes
TODO: Eight colours by role (outline, bark dark, bark, leaf dark, leaf, leaf lit, cone, pollen) built from five bases; shades shift hue towards blue and lights towards yellow, as pixel artists do (shade() in palette.js). The Yours palette: lock colours, generate the rest in OKLCH (Ottosson 2020) with a seed, per-slot limits so the outline stays dark. Snippets: shade() and the scoring loop of generatePalette. Link the palette research note or the playground.
7. A sprite sheet
Because the generator is fast (about 2 to 13 ms per tree in Node, depending on age) and the pixel renderer faster still (under 1 ms at 32 px), the page can build a whole sheet on demand: four seeds by four ages, at 16 or 32 pixels, named after its seeds.
const SHEET_AGES = [0.15, 0.4, 0.7, 1];
…
for (let row = 0; row < 4; row++) {
SHEET_AGES.forEach((age, col) => {
const s = renderPixel(generate({ ...p, seed: p.seed + row, age }), { size, yaw: view.yaw, outline: view.outline });
g.putImageData(spriteImage(s), col * size, row * size);
});
}
download(c, `araucaria_sheet_seed${p.seed}-${p.seed + 3}_${size}px.png`); Going further
- Another grammar. The Chilean wine palm has no whorls at all: a thick, bulging trunk and pinnate leaves. If the description format survives it, it is general enough.
- A mesh. Segments become tapered cylinders and scale leaves become instanced cards, with no change to the generator.
- Port to C#. Flat lists and pure functions map straight onto structs and arrays for Unity, so the trees can grow in the world of Islands.
- A forest. In the Araucaria Forest this tree grows by the hundred, at every age, on four point distributions. ✦ IA
La idea en palabras simples
El generador no dibuja un árbol. Anota de qué está hecho: un tronco, verticilos de ramas, unas pocas ramillas, miles de hojas en escama y, en los árboles adultos, conos. Cada pieza tiene un tipo, una posición en 3D, un radio y un nombre de color. Dibujar es trabajo de otros, y en esta página hay dos dibujantes: uno con líneas y figuras y otro en pixel art.
Piensa en una receta y dos cocineros. La receta dice «una columna y luego anillos de ramas»; un cocinero la hace con manga pastelera y el otro con Lego. La analogía tiene límites: una receta deja margen al gusto de cada cocinero, mientras que aquí la descripción fija cada rama, y los dibujantes solo deciden cuánto mostrar de ella.
El árbol es un buen primer tema porque sus reglas son pocas y se ven. Las ramas salen del tronco en verticilos, varias a la misma altura, y casi no se ramifican. Cada rama está cubierta de hojas en escama, rígidas, punzantes y triangulares, así que parece una cuerda verde. La forma cambia con la edad: los jóvenes son pirámides con verticilos hasta el suelo; los viejos pierden las ramas bajas y quedan con una columna desnuda y una copa en paraguas. Lleva la Edad de 0 a 100 % y mira cómo la siguen los dos dibujantes.
Por qué importa
El pewén es uno de los árboles de los Andes del centro-sur de Chile y de la cordillera de Nahuelbuta, y un vínculo vivo con una familia muy antigua. El polen de las Araucariaceae se remonta al Triásico; en Sudamérica y la Antártida los macrofósiles fiables más antiguos son del Jurásico temprano, y la familia tuvo su mayor extensión en el Cretácico temprano, cuando llegó por el norte hasta Colombia y Brasil (Panti y colaboradores, 2012). El bosque petrificado de Cerro Cuadrado, en la Patagonia argentina, guarda conos de Araucaria mirabilis del Jurásico medio a superior. «Fósil viviente» es una etiqueta divulgativa, no un término técnico: lo antiguo es el linaje, no la especie.
Los árboles suelen ser dioicos: unos son machos, con conos de polen alargados, y otros hembras, con conos globosos que maduran unos 18 meses después de la polinización según Wikipedia, o en 2–3 años según la Gymnosperm Database. Las semillas, los piñones o ngulliw en mapudungun, son alimento: los pewenche, «la gente del pewén», toman de ahí su nombre, y la Gymnosperm Database cuenta que en algunas comunidades mapuche los piñones son el 10–15 % de la dieta en la temporada de cosecha. La corteza puede tener 15 cm de grosor, quizá una adaptación al fuego. Chile la declaró monumento natural en 1990 (decreto 43 del Ministerio de Agricultura), lo que prohíbe talarla, y la UICN la clasifica En Peligro desde 2013. Usa los botones de Sexo para cambiar el tipo de cono; por debajo del 45 % de edad no hay conos.
Para un juego, separar describir de dibujar sirve para reutilizar. La misma descripción alimenta el dibujo de líneas que ves aquí, un sprite de 16 píxeles para un mapa, uno de 32 para una vista más cercana y, más adelante, una malla 3D, todo desde una semilla. Así un bosque sigue siendo el mismo bosque con cualquier cámara.
¿Hay un teorema?
Ningún teorema dice cómo crece una araucaria. Lo que hay son mediciones, y una importa aquí. Es tentador leer la edad de un árbol en sus verticilos, uno por año. Lusk y Le-Quesne (2000) lo comprobaron en araucarias jóvenes de la cordillera de la Costa y resultó falso: los 35 árboles tenían más anillos de crecimiento que verticilos, con un verticilo cada 2,1 años al sol y cada 2,6 a la sombra. Por eso la Edad de esta página es una fase de vida de 0 a 100 % y no un número de años, y por eso la separación entre verticilos tiene su propio deslizador.
En lo generativo, la referencia clásica es The Algorithmic Beauty of Plants (1990), de Prusinkiewicz y Lindenmayer, que construye plantas con reglas de reescritura (sistemas L). Esta página usa algo más simple: un bucle explícito sobre verticilos y ramas, porque la estructura de la araucaria es lo bastante regular como para no necesitar una gramática.
Las ecuaciones
Alturas en metros. H es la altura adulta, a la edad en [0, 1] y s(·) la curva smoothstep de 0 a 1.
- Altura actual: h = H · (0,12 + 0,88 · s(a / 0,75))
- Parte desnuda del tronco: b = 0,8 · s((a − 0,25) / 0,6)
- Largo de la rama a una profundidad d = h − y bajo el ápice: L(d) = mín(W, 0,3 + d / tan p), con W el ancho de la copa (la rama más larga)
- Pendiente de la copa: p = 65° + (P − 65°) · s((a − 0,35) / 0,6), con P la pendiente de la copa vieja
- Elevación a lo largo de la rama, t de 0 a 1: θ(t) = ángulo − caída + curva · t²
- Escamas alrededor de una rama: φk = k · 137,5°
El largo de la rama, en palabras: vista de lado, la copa es un triángulo. Las ramas crecen en línea recta con su profundidad bajo el ápice, con la pendiente p, hasta llegar al ancho de copa W; más abajo, todas son largas. El árbol joven tiene una pendiente empinada, una pirámide; con la edad baja a P, y la copa baja ya no está (la parte desnuda). Lo que queda es un triángulo bajo y ancho sobre una columna desnuda: el paraguas.
Ese triángulo es la copa joven y la opción Triángulo. La copa vieja sigue un perfil (Corazón por defecto, cada figura con sus dos deslizadores; Mostrar la figura guía lo dibuja sobre el árbol): con la edad, cada rama se acerca al largo de un óvalo aplastado (Disco) o de dos óvalos, un parasol y debajo un disco colgante más ralo (Dos discos), una cardioide aplastada cuyas ramas bajas cuelgan en dos lóbulos (Corazón) o un disco cóncavo con las puntas hacia arriba y un lóbulo debajo (Cuenco); su caída cambia con él. Los ramilletes juntan el follaje en las puntas: el tramo interior de la rama queda casi desnudo y el último tercio se abre en un cono de ramillas de largos desiguales. ✦ IA
Lo que el dibujo dice y lo que no
- Las curvas de edad son un modelo, no una tabla de crecimiento. Reproducen las formas conocidas (pirámide y luego paraguas) y las alturas (30–40 m), no tasas de crecimiento medidas; las proporciones de la copa, unas dos veces más ancha que alta al 100 %, están ajustadas a ojo.
- El árbol está a escala (la figura gris mide 1,7 m), pero los órganos no: las escamas se dibujan unas cuatro veces más grandes y los conos unas dos, o desaparecerían a este zoom.
- Ramas por verticilo: una fuente da de 5 a 7 y no encontré una medición primaria. El deslizador va de 3 a 9.
- Las ramillas se simplifican a pares en la parte externa de cada rama, y los conos aparecen por encima del 45 % de edad por regla del modelo, no por una edad documentada de la primera fructificación.
- En pixel art el árbol llena el sprite sea cual sea su edad, así que un renoval y un árbol viejo no están a escala entre sí. El viento solo mueve el dibujo de líneas.
TODO: Un punto de límites para los sprites estilizados: exageran a propósito (brazos de perfil, penachos en las puntas, tronco aplastado al 45 %, copa hasta 1,6 veces más ancha que alta), así que cambian realismo por una silueta legible. Decir que es una decisión artística y enlazar la idea de dirección artística.
Cómo funciona
Esta parte recorre el código de la escena paso a paso. Cubre:
- Una descripción de la planta que cualquier renderizador puede leer.
- La edad como un puñado de curvas.
- Verticilos y largos de rama.
- Ramas como polilíneas y escamas en una espiral de ángulo áureo.
- Un renderizador de líneas con profundidad y viento.
- Un renderizador de pixel art con su propio nivel de detalle.
- Un renderizador de pixel art estilizado que mantiene los brazos legibles.
- Paletas: fijas y la tuya, generada en OKLCH.
- Exportar una hoja de sprites.
JavaScript sin más y la API Canvas 2D, sin librerías. El generador y los dos renderizadores suman unas 470 líneas y no tocan el DOM, así que corren en Node para pruebas y mediciones; el script de la página, unas 220. Los comentarios del código están en inglés.
TODO: Actualizar el número de líneas (el renderizador estilizado, palette.js y palette-picker.js son nuevos).
1. Una descripción, no un dibujo
La planta son dos listas planas. Las partes (parts) son lo leñoso, con sus dos
extremos en 3D y un radio en cada uno; los detalles (details) son hojas y conos,
con posición, dirección y tamaño. Cada entrada apunta a su padre por índice en vez de anidarse, así que las listas son
fáciles de recorrer, de guardar como JSON y, más adelante, de portar a C# como arrays de
structs. Los colores son nombres («bark», «leaf»); cada renderizador tiene su paleta.
// A part is anything with volume along an axis: a trunk piece, a branch, a bone.
// a, b: ends in metres (y up); r0, r1: radius at each end. parent: index of the part it
// hangs from, -1 for a root. group: the whorl. role: a colour name, not a hex.
export function addPart(art, p) {
art.parts.push({ parent: -1, group: -1, tone: 0.5, ...p });
return art.parts.length - 1;
}
// A detail is small and countless: a scale leaf, a cone. shape tells any renderer how to
// draw it without knowing the species: "tick" (a short line along dir) or "disc".
export function addDetail(art, d) {
art.details.push({ part: -1, tone: 0.5, shape: "disc", ...d });
return art.details.length - 1;
} Las posiciones son 3D desde el principio, con el tronco en el eje y. La vista lateral es solo una proyección: arrastra el árbol y gira, porque los renderizadores rotan los puntos alrededor del tronco antes de descartar la profundidad.
2. La edad como curvas
Todo lo que cambia con la edad sale de una función pequeña: la altura actual, cuánto tronco queda desnudo, cuánto llegan las ramas y qué tan empinada es la copa. Las curvas smoothstep hacen que cada cambio sea gradual, así que el deslizador nunca da saltos.
export function stage(p) {
const h = p.height * (0.12 + 0.88 * smoothstep(0, 0.75, p.age)); // current height
const bare = 0.8 * smoothstep(0.25, 0.85, p.age); // bare share of the trunk
const reach = p.crownWidth; // longest branch
const pitch = 65 + (p.crownPitch - 65) * smoothstep(0.35, 0.95, p.age); // a steep pyramid flattens into an umbrella
return { h, bare, reach, pitch, cones: p.age >= 0.45 };
} 3. Verticilos
Los verticilos se colocan desde el ápice hacia abajo, con la separación elegida y un poco de jitter, hasta llegar a la parte desnuda del tronco. Cada verticilo gira al azar, y cada rama recibe un pequeño desvío de ángulo, una variación de largo y una probabilidad de faltar, mayor en la base de una copa vieja. Todo el azar sale del hash de Islands Lab (semilla, índice, sal), así que una rama conserva su forma cuando cambias algo que no le afecta.
const crownBottom = st.bare * st.h;
const count = Math.floor((st.h - 0.3) / p.spacing);
for (let i = 0; i < count; i++) {
const y = st.h - 0.3 - (i + (u01(s, i, SALT.whorl) - 0.5) * 0.3) * p.spacing;
if (y < Math.max(0.15, crownBottom)) break;
const d = st.h - y; // distance below the apex
const length = Math.min(st.reach, 0.3 + d / Math.tan(st.pitch * RAD));
const crownT = (y - crownBottom) / Math.max(1e-6, st.h - crownBottom); // 0 at the crown base, 1 at the top
const droop = p.age * 35 * (1 - crownT); // lower crown branches hang lower in old trees
const offset = u01(s, i, SALT.az) * 2 * Math.PI;
…
for (let j = 0; j < p.perWhorl; j++) {
const id = i * 16 + j;
// Old trees lose branches at the bottom of the crown.
if (u01(s, id, SALT.miss) < 0.04 + 0.35 * p.age * (1 - crownT)) continue;
const az = offset + ((j + (u01(s, id, SALT.ang) - 0.5) * 0.3) / p.perWhorl) * 2 * Math.PI;
const L = length * (0.85 + 0.3 * u01(s, id, SALT.len));
…
}
} Mueve la Separación entre verticilos: el árbol conserva el contorno pero cambia el número de pisos.
4. Ramas y escamas
Una rama son ocho segmentos. Su elevación empieza en el ángulo de la rama, menos la caída de la copa baja, y sube hacia la punta con el cuadrado de la distancia: la punta algo alzada de las ramas de la araucaria.
for (let k = 0; k < n; k++) {
const t = (k + 0.5) / n;
const elev = (p.angle - b.droop + p.curve * t * t) * RAD;
const next = add3(pos, [dirH[0] * Math.cos(elev) * step, Math.sin(elev) * step, dirH[2] * Math.cos(elev) * step]);
…
parent = addPart(plant, { kind: "branch", a: pos, b: next, r0: ra, r1: rb, role: "bark", group: b.whorl, parent, tone });
scales(plant, p, pos, next, ra, parent, b.whorl, b.id * 64 + k);
…
pos = next;
} Las escamas envuelven cada segmento en espiral, cada una girada 137,5° respecto a la anterior: el ángulo áureo, que nunca alinea las hojas en filas. Es la misma razón áurea de la secuencia R1 de la parte 5 de Islands Lab. Cada hoja apunta hacia fuera y hacia delante, a lo largo de la rama.
for (let k = 0; k < count; k++) {
const t = (k + 0.5) / count, ang = (key * 7 + k) * GOLDEN;
const out = add3(mul3(u, Math.cos(ang)), mul3(v, Math.sin(ang)));
const pos = add3([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t], mul3(out, r));
addDetail(plant, { kind: "scale", shape: "tick", p: pos, dir: norm3(add3(out, mul3(axis, 0.8))), size: 0.04, role: "leaf",
part: segment, tone: u01(p.seed, key * 97 + k, SALT.tone) });
} ¿Las hojas de la araucaria real siguen el ángulo áureo?
Las fuentes dicen que las hojas están en espiral, no con qué ángulo. El ángulo áureo es una elección habitual en los generadores porque reparte las hojas de forma pareja; aquí es una suposición.
5. Líneas y figuras
El renderizador de líneas proyecta cada segmento y cada órgano, los reparte en seis cubetas de profundidad y las dibuja de atrás hacia delante, fundiendo las lejanas con el fondo. Dentro de una cubeta, todas las escamas de un mismo tono van en un solo trazo, lo que permite mover unas 10.000 hojas con fluidez.
// Depth buckets, back to front, so the near side covers the far side but strokes of one
// colour can still be batched inside a bucket.
const BUCKETS = 6;
const bucketOf = (depth) => Math.max(0, Math.min(BUCKETS - 1, Math.floor(((depth / reach) * 0.5 + 0.5) * BUCKETS)));
…
const tick = Math.max(2.2 * dpr, 0.04 * view.scale * 4); // scales drawn about 4× life size, never under 2 px
for (const o of plant.details) {
const a = toScreen(o.p, detailGroup(plant, o));
const bk = buckets[bucketOf(a.depth)];
if (o.kind === "scale") {
const d = project(o.dir, view.yaw);
bk.leaf[Math.min(2, Math.floor(o.tone * 3))].push([a.x, a.y, a.x + d.x * tick, a.y - d.y * tick]);
} else {
bk.cones.push([a, o]);
}
} El viento no está en la descripción. El renderizador mueve cada punto al dibujarlo: el tronco se dobla con el cuadrado de la altura y las ramas se agitan según su distancia al tronco, con dos senos, como en Hillside. Un renderizador futuro puede ignorarlo o hacerlo en la GPU.
export function windOffset(p, whorl, height, time, strength) {
if (!strength) return [0, 0];
const h = p[1] / height;
const trunk = strength * 0.012 * height * h * h * (Math.sin(time * 0.9) * 0.7 + Math.sin(time * 2.1 + 1.3) * 0.3);
const r = Math.hypot(p[0], p[2]);
const flutter = whorl < 0 ? 0 : strength * 0.04 * r * Math.sin(time * 2.6 + whorl * 0.9 + r * 0.6);
return [trunk + flutter * 0.4, flutter];
} 6. El pixel art tiene su propio nivel de detalle
Reducir el dibujo de líneas a 16 píxeles da una mancha gris: miles de trazos más pequeños que un píxel, promediados. El renderizador de pixel art lee la misma descripción pero decide otra cosa. Ajusta el árbol al sprite, dibuja cada rama como una línea de al menos un píxel de ancho con el algoritmo de Bresenham (1965) y deja que los verticilos se fundan en una masa donde están a menos de un píxel. Las ramas del lado lejano van a su propia capa, para poder oscurecerlas.
// Branches first, as thin lines: at these sizes a branch and its scales are one pixel
// wide, and whorls closer than a pixel merge into a solid crown on their own.
const leafR = 0.12; // branch radius plus scale length, roughly, in metres
for (const s of plant.parts) {
if (s.kind === "trunk") continue;
const a = project(s.a, yaw), c = project(s.b, yaw);
const v = (a.depth + c.depth) / 2 < -0.15 * reachX ? BACK : FRONT;
line(px(a.x), py(a.y), px(c.x), py(c.y), Math.max(1, Math.round(2 * (s.r0 + leafR) * k)), (x, y) => put(x, y, v));
}
…
// Close one-pixel gaps in the crown, across and down, so small sprites read as a mass.
closeGaps(layer, size); Luego cada píxel recibe uno de ocho colores, sin mezclas: iluminado donde el píxel de arriba está vacío, en sombra donde lo está el de abajo, el lado lejano siempre en sombra y el tronco en dos tonos cuando es lo bastante ancho. Unos pocos conos se vuelven píxeles sueltos, y un contorno rodea el exterior.
const v = at(x, y);
if (!v) continue;
let c;
if (v === TRUNK) c = (x >= cx && trunkWidth(layer, size, y) > 1) || at(x, y - 1) === FRONT ? BARK_D : BARK;
else if (v === BACK) c = LEAF_D;
else if (!at(x, y - 1)) c = LEAF_L;
else if (!at(x, y + 1)) c = LEAF_D;
else c = LEAF;
data[y * size + x] = c; Cambia a 16 px y mueve la Edad: el renoval es una pila de pisos, el adulto un cono y el viejo una losa sobre un palo, que es más o menos lo que puede ser una araucaria a ese tamaño.
TODO: Desde el 2026-10-29 la página dibuja solo el sprite de Grumos (render-pixel-silhouette.js: la figura guía como máscara, una mata por punta de rama, elegido en la puerta). Las secciones 6 y 6b describen sus dos antecesores, el sprite fiel y el estilizado: presentarlos como los pasos que llevaron a él y añadir una sección sobre los grumos (primero la silueta, exagerar el rasgo, matas; ver research/2026-10-29-pixel-art-tecnicas.md).
6b. La estética antes que la exactitud: el sprite estilizado
TODO: Por qué el sprite fiel pierde los brazos a 16 y 32 px, y la regla: conservar el rasgo más significativo. Cómo: pisos elegidos por separación en píxeles, cada brazo de perfil, solo los brazos extremos a 16 px, un penacho (elipse) en cada punta y a media rama en las ramas largas a 32 px, tronco desnudo aplastado al 45 %, estiramiento horizontal hasta 1,6, remate en la punta y contorno. Fragmento de render-pixel-stylized.js. Imagen antes/después a 16 y 32 px.
6c. Paletas
TODO: Ocho colores por papel (contorno, tronco oscuro, tronco, hoja oscura, hoja, hoja iluminada, cono, polen) a partir de cinco bases; las sombras giran el tono hacia el azul y las luces hacia el amarillo, como hacen los pixel artists (shade() en palette.js). La paleta Tuya: fijar colores, generar el resto en OKLCH (Ottosson 2020) con semilla, límites por casilla para que el contorno siga oscuro. Fragmentos: shade() y el bucle de puntuación de generatePalette. Enlazar la nota de investigación de paletas o el taller.
7. Una hoja de sprites
Como el generador es rápido (de 2 a 13 ms por árbol en Node, según la edad) y el renderizador de pixel art más todavía (menos de 1 ms a 32 px), la página puede construir una hoja entera al momento: cuatro semillas por cuatro edades, a 16 o 32 píxeles, con las semillas en el nombre del archivo.
const SHEET_AGES = [0.15, 0.4, 0.7, 1];
…
for (let row = 0; row < 4; row++) {
SHEET_AGES.forEach((age, col) => {
const s = renderPixel(generate({ ...p, seed: p.seed + row, age }), { size, yaw: view.yaw, outline: view.outline });
g.putImageData(spriteImage(s), col * size, row * size);
});
}
download(c, `araucaria_sheet_seed${p.seed}-${p.seed + 3}_${size}px.png`); Para ir más allá
- Otra gramática. La palma chilena no tiene verticilos: un tronco grueso y abombado y hojas pinnadas. Si el formato de la descripción la aguanta, es lo bastante general.
- Una malla. Los segmentos pasan a ser cilindros cónicos y las escamas, tarjetas instanciadas, sin tocar el generador.
- Portar a C#. Listas planas y funciones puras se traducen directamente a structs y arrays en Unity, para que los árboles crezcan en el mundo de Islands.
- Un bosque. En el Bosque de araucarias este árbol crece por cientos, a todas las edades, sobre cuatro distribuciones de puntos. ✦ IA
SourcesFuentes
- "Araucaria araucana", Wikipedia. Habit with age, leaves living up to 24 years, cones maturing about 18 months after pollination, bark up to 15 cm thick, Pehuenche name and diet. en.wikipedia.org/wiki/Araucaria_araucana
- C. J. Earle (ed.), "Araucaria araucana", The Gymnosperm Database. Height to 40 m (a record 51.19 m), 150 cm dbh, leaves 30–50 × 8–25 mm persisting 10–15 years, seed cones 10–18 cm, pollen cones 7–15 cm, mostly dioecious, seeds in the Mapuche diet. conifers.org/ar/Araucaria_araucana.php
- "Araucaria araucana", LLIFLE Encyclopedia of trees. Branches in whorls of 5 to 7 (the only source found for the count). llifle.com/Encyclopedia/TREES/Family/Araucariaceae/30780/Araucaria_araucana
- C. H. Lusk, C. Le-Quesne, "Branch whorls of juvenile Araucaria araucana (Molina) Koch: are they formed annually?", Revista Chilena de Historia Natural 73(3) (2000). Whorls were not annual: 2.1 years per whorl in the sun, 2.6 in the shade. scielo.cl/scielo.php?script=sci_arttext&pid=S0716-078X2000000300013
- A. Premoli, P. Quiroga, M. Gardner, "Araucaria araucana", IUCN Red List of Threatened Species 2013: e.T31355A2805113. Endangered. dx.doi.org/10.2305/IUCN.UK.2013-1.RLTS.T31355A2805113.en
- Chile, Ministerio de Agricultura, Decreto N° 43 (Diario Oficial, 3 April 1990): declares Araucaria araucana a natural monument. FAOLEX record. leap.unep.org/en/countries/cl/national-legislation/decreto-no-43-declara-monumento-natural-la-araucaria-araucana
- C. Panti, R. R. Pujana, M. C. Zamaloa, E. J. Romero, "Araucariaceae macrofossil record from South America and Antarctica", Alcheringa 36(1) (2012) 1–22. doi.org/10.1080/03115518.2011.564562
- UCMP Berkeley, "Araucariaceae: fossil record". Triassic cone scales; Araucaria mirabilis from the Cerro Cuadrado petrified forest, Middle to Late Jurassic. ucmp.berkeley.edu/seedplants/conifers/araucariafr.html
- P. Prusinkiewicz, A. Lindenmayer, The Algorithmic Beauty of Plants, Springer (1990). The classic on generating plants from rules. algorithmicbotany.org/papers/#abop
- J. E. Bresenham, "Algorithm for computer control of a digital plotter", IBM Systems Journal 4(1) (1965) 25–30. The line drawing behind the sprites. doi.org/10.1147/sj.41.0025