(¦3[▓▓]=) RJean Lee


back to blog

Locating Place|Space: Watercolor Shaders and Generative Nature Study in p5.js


A generative landscape with redwood trunks, layered green hills, and an orange sunset.
2026-09-10
p5.js, Generative Art, Shader, Procedural Landscape

Locating Place|Space: Watercolor Shaders and Generative Nature Study in p5.js

本文介紹一項以 p5.js 建立的程序式自然景觀研究,將同伴提供的文字描述轉譯為水彩天空、層疊山丘、小樹與高聳紅木所構成的場景。系統使用 p5.js 2.0 shader、noise 函數、混合模式與程序式幾何建立畫面的質感與深度。Gaussian 函數用來塑造山丘與樹幹,Fibonacci 數列則協助安排景觀中的元素。透過草圖與參數測試,本研究探索語言、個人記憶、數學與程式碼如何共同詮釋一個想像中的地方。

This article presents a generative nature study built with p5.js from a landscape prompt written by a partner. The scene combines a watercolor sky, layered hills, small trees, and tall redwood trees. A p5.js 2.0 shader, noise functions, blend modes, and procedural geometry create the visual texture and depth. Gaussian functions shape the hills and tree trunks, while a Fibonacci sequence helps organize elements across the landscape. Through sketches and parameter tests, the process explores how language, personal memory, mathematics, and code can work together to interpret an imagined place.


Prompt and Interpretation

Queenie described a wide view from the top of a mountain at sunset. Emerald redwoods, oak woodlands, Douglas firs, and rolling hills fill the foreground. In the distance, clouds cover the sky, while pink, blue, and orange light reaches the hills.

Assignment instruction
The assignment instruction for the Locating Place and Space study.

I interpreted the description as five spatial layers: sky, sun, hills, trees on the hills, and redwood trees in the foreground. J. M. W. Turner’s paintings also influenced the wet-on-wet watercolor quality of the sky.

Result and Reflection

View the interactive sketch on OpenProcessing
The final generative redwood landscape at sunset.
Reflection

I enjoyed the prompt because I had visited a redwood forest one month earlier. My own experience was from inside the forest, but Queenie’s description gave me a view that moved from far away to nearby details and then back into the distance. Her careful list of tree types and description of warm light gave the scene both structure and emotion.

I began with a spatial analysis and divided the image into five parts. The sky uses a watercolor shader and noise texture. The sun uses resetShader() and a blend mode so it can overlap with the sky. The hills use custom spline shapes, noise, a Gaussian function, and a Fibonacci sequence. Small trees use four spline vertices, while the redwoods extend from the top to the bottom of the canvas.

Because I did not see the original image, my past experience shaped the interpretation. I connected the forms in the prompt with mathematical functions I knew, then spent time adjusting colors and parameters to recreate the peaceful feeling I imagined.

Feedback

The class feedback noted the connection between shader code and nature-inspired elements, and highlighted the use of the golden ratio to arrange the mountains. It also identified an unintended movement in the trees and suggested that a more stable scene might feel less algorithmic and more peaceful. Further suggestions included studying morphogenesis algorithms and p5.js compute shaders.


Building the Watercolor Sky

I started with the p5.js 2.0 shader example and used a noise-based shader to draw the sky on a rectangle.

The p5.js shader example.
An early watercolor sky generated with a shader.

Draft

A watercolor landscape draft with notes about the shader and color layers.

Noise Scale

I tested several noise scales to compare large color fields with fine texture.

Shader test with noise scale 10.
noise scale = 10
Shader test with noise scale 5.
noise scale = 5
Shader test with noise scale 0.5.
noise scale = 0.5
A watercolor shader result after adjusting the noise scale.

Screen Blend Mode

I added a small noise texture and used screen blending to brighten the combined color layers.

A fine noise texture with a scale of 50.
Small noise texture, noise scale = 50
The watercolor texture before screen blending.
Before screen blend
The watercolor texture after screen blending.
After screen blend
let screenColor = [1, 1, 1, 1] -
  (([1, 1, 1, 1] - mix3) * ([1, 1, 1, 1] - mixb));
A screen blend test with watercolor textures.
The watercolor shader applied to a larger rectangle.

Adding the Sun

A sun drawn with screen blend mode.
SCREEN
A sun drawn with overlay blend mode.
OVERLAY

Generating the Hills

A basic hill made with beginShape, vertex, and endShape.

Gaussian Curves and Spline Vertices

Read the p5.js splineVertex() reference
A hand-drawn Gaussian curve study.
A hill changing from straight vertices to spline vertices.
vertex() to splineVertex()
function gaussianPDF(x, peak = 0.5, width = 0.3) {
  const exponent = -((x - peak) ** 2) / (2 * width ** 2);
  return Math.exp(exponent);
}
A hill shape with watercolor texture.
Several textured hills layered into a landscape.

Color Variation

I used HSB color values to create related variations from one base color.

Read the p5.js colorMode() reference
A hill and sun test using HSB color variations.
code
function expandColor(clr, fac = 1) {
  const clrArr = [];
  push();
  colorMode(HSB);
  const scl = 10;
  const hScl = scl * fac;

  for (let i = 0; i < 4; i++) {
    const h = hue(color(clr));
    const s = saturation(color(clr));
    const b = brightness(color(clr));
    const newClr = color(
      h + random(-hScl, hScl),
      s + random(-scl, scl),
      b + random(-scl, scl)
    );
    clrArr.push(newClr.toString('#rrggbb'));
  }
  pop();
  return clrArr;
}

Noise Seed

Read the p5.js noiseSeed() reference
Layered hills before adding a fixed noise seed.
Without a fixed noise seed
Layered hills after adding a fixed noise seed.
With noiseSeed()

Fibonacci Distribution

I used Fibonacci values to reduce the number of hills in rows that are farther from the viewer.

A Fibonacci spiral reference diagram.
Hills distributed in rows with Fibonacci values.
function fibonacci(n) {
  let a = 0;
  let b = 1;
  for (let i = 0; i < n; i++) {
    [a, b] = [b, a + b];
  }
  return a;
}

Parameter Studies

A test of hill width parameters.
Width
A test of hill height parameters.
Height
A test of hill span and amplitude parameters.
Span and amplitude
A landscape created after adjusting hill parameters.
Hill distribution code
const row = 6;
for (let i = 0; i < row; i++) {
  const num = fibonacci(2 + row - i);

  for (let j = 0; j < num; j++) {
    const hill = new RJ_Hill({
      x: random(-width / 2 - padding, width / 2 - padding),
      y: -height / 6 + i * 50 + random(50) + 20,
      w: pow(i, 2) * 30 + random(20, 50),
      h: pow(i, 2) * 3,
      a: random(0.5, 1),
      noiseScale: 1,
      span: i * 3 + 5,
      ampScl: i * 20 + 20
    });
    list_hill.push(hill);
  }
}

Refining the Composition

I continued testing the palette, row count, position, scale, and sun placement until the layers felt deeper and more balanced.

A green color palette test for the hills.
A landscape with many layered green hills.
Layered green hills with an orange sun.
The final hill distribution used in the landscape.

Drawing Trees

The first tree leaf shape test.
The second tree leaf shape test.
The third tree leaf shape test.
Small trees distributed across the layered hills.
Tree shape code
beginShape();
splineVertex(0, 0, 0.5, 0);
splineVertex(-this.w / 2 + random(-this.ampScl, this.ampScl),
  this.h / 2 + random(-this.ampScl, this.ampScl), 0, 0.5);
splineVertex(0, this.h, 0.5, 1);
splineVertex(this.w / 2 + random(-this.ampScl, this.ampScl),
  this.h / 2 + random(-this.ampScl, this.ampScl), 1, 0.5);
endShape(CLOSE);

Constructing the Redwood Trees

A hand-drawn study of a redwood trunk using a Gaussian curve.
Generated redwood trunks in front of the finished landscape.
Redwood shape code
beginShape();
splineVertex(0, 0, 0, 0);

for (let i = 0; i < this.span; i++) {
  const x = -this.w * gaussianPDF(i / this.span * this.guassianFac);
  const v = i / this.span;
  splineVertex(x + random(-xShift, xShift), this.h * v, 0, v);
}

for (let i = this.span - 1; i >= 0; i--) {
  const x = this.w * gaussianPDF(i / this.span * this.guassianFac);
  const v = i / this.span;
  splineVertex(x + random(-xShift, xShift), this.h * v, 1, v);
}

endShape(CLOSE);

AI Assistance

AI support was used to explain a p5.js shader example, draft Gaussian and Fibonacci functions, send parameters to a shader with uniforms, convert hexadecimal colors for shader input, and return from a custom shader before using blendMode().