Making landscapes with code

Title
How to make landscapes using code
Published
July 5, 2022
The finished piece we’ll be talking about,
The finished piece we’ll be talking about, Green Like No Grass Is Green

My favorite thing about generative art is that the biggest difference between the image on the right and the finished piece above is about 1.5 million repetitions.

One low-res line with a halfhearted gradient? Unremarkable. But a couple million lines with a halfhearted gradient? Could be interesting.

This article is about how you turn the lightly battered jpeg on the right into the verdant Elysium above.

image

The first step is to draw a bunch of them. Let’s randomly vary the lengths so they’re a little less samey.

How many should we draw? Not sure.

Where should we draw them? Anywhere as long as it’s not the same point over and over again. Maybe we can fake some perspective by putting them in a parallelogram.

image

Okay, that looks like something. Still a little sparse though. What if we tripled the number of lines?

image

Huh. That actually looks kinda like grass. I love how simple this is. We’ve halfheartedly faked two things — shadow and perspective — and my brain is already going, “Yep, totally a soft patch of grass. You should fall asleep in it.”

This is exactly how the piece started. I was watching this MagicaVoxel video and noticed that the beautiful grass was just straight lines that got darker at the bottom. So I figured, “I can draw lines!” and now here we are reading a self-congratulating blog post about how beautiful my lines are.

From
From ArtChanny on YouTube

After taking a nap curled up on top of my computer screen, I wondered what else you could do with these surprising little lines. What about some hills?

image

Here’s how you draw fake terrain like this:

  1. Pick out a grid of points. These will be the starting points for your lines.
  2. Feed the x and y values into a simplex noise function, or something like it.
  3. This will give you a noise value which we’ll treat as the height for the terrain. But since we’re not trying to get into 3d math, we’ll just use our fake height to adjust the y value of the point.

In JavaScript it looks something like this.

const points = [{ x: 0, y: 0 }, { x: 1, y: 0 }, /* etc. */]

for (const point of points) {
  // noise is a number between -1 and 1
	const noise = noise.simplex2d(point.x, point.y)

  // Multiply by some arbitrary number so your hills look good.
  const height = noise * 0.2

  point.distanceFromCamera = point.y
	point.y += height
}

There’s also one other trick. When you draw the lines, start with the ones farthest away from the camera. For us, “farthest away from the camera” means “the original y value is closest to the top of the canvas.” If you do this, later lines cover up earlier ones, creating the illusion of depth.

Okay, let’s see what else we can do with this. The geometry is looking alright, so let’s play with color. Since we know that distant objects get desaturated, let’s try implementing that. (This is called “atmospheric perspective.”)

image

Looks okay! Next up, some sky. I don’t love how it looks as a flat blue — it’s too separate from the grass. But if we use a bunch of lines, it looks a little better, and if we lighten up the horizon, it looks better still.

Let’s also randomly vary our grass color a little bit.

Flat blue
Flat blue
Adding lines
Adding lines
Brightening the horizon
Brightening the horizon

I stole this horizon brightening trick from Kawase Hasui, one of my favorite artists. His piece is also a great example of atmospheric perspective. Notice how the fields near the camera are darker and more saturated than the fields farther away.

Kawase Hasui,
Kawase Hasui, The Onsen Range Seen From Amakusa

Our hills don’t really have a sense of distance yet. It looks like we’re zoomed way in on a small piece of grass. But we can fix that by decreasing the amplitude of our noise function as grass gets further away from the camera.

Let’s also tweak the colors of our grass and sky, and make our atmospheric perspective more noticeable.

image

Up until now we’ve been trying to get an image that didn’t look obviously wrong. And I think we’ve succeeded. So let’s change directions and see if we can make the piece weirder and more interesting.

What about some more vibrant colors?

image

That’s kind of a lot. I like it, but maybe it should only go in the valleys? And maybe we can try multiple color palettes for different valleys. Oh, and we can add just a sprinkle of our more vibrant colors back into the regular grass palette too.

Adding color in the valleys
Adding color in the valleys
Adding more vibrant colors to the default grass palette
Adding more vibrant colors to the default grass palette

What if we added a dusting of more vibrant colors to the sky?

image

It’s subtle, but I like how it adds just a touch more depth.

At this point the colors are nice, and it does a good job of representing the thing it’s supposed to represent. But I’d love for it to be a little weirder. And I’d like to show more of its three dimensionality. How about some roads?

image

Here I’m using a threshold for the noise function. Check if the height is between two values and if so, delete the line. The roads are black because of the fake shadows at the bottom of each line.

This is pretty cool, but is there anything else we could do with the roads? Maybe we could paint them with something super saturated?

image

Feels like a bit much. What about white?

image

This works better, I think. It still feels strange and otherworldly, but since it keeps the same color vocabulary as the rest of the piece, it fits together a little better. There’s also some nice contrast — the rest of the piece is all muted color, and this creates a break from that.

We’re basically done. The last thing is to play with aspect ratio, zoom, and margin size. A few highlights from that process:

Portrait orientation
Portrait orientation
Landscape orientation
Landscape orientation
No margin
No margin

And then, after much tweaking, the final version:

image

I’ll leave you with a short gif of all 366 outputs I generated while working on this piece, including some blips, bloops, and dead ends we didn’t talk about in this article.