Mianzhi Wang

Ph.D. in Electrical Engineering

MATLAB Plots Can Be Beautiful II - Mountains


Randomness do not always lead to unwanted noise. Randomness is closely related to procedural generation in game industry. With an initial seed, one can generate random fantasy map, a random planet, a random block world, or even an random universe. Randomness can also used to create beautiful computer arts, as seen on OpenProcessing. In this article, I will give a brief tutorial on how to create good looking mountains in MATLAB using the power of randomness. Here is a glimpse of the final product:

mountains final

Setting up the basics

We start by writing down the boilerplate code and defining the necessary parameters we will use later. The description of each parameter is given in the comments.

function plot_mountains
%PLOT_MOUNTAINS Plots mountains.

% set up figure
figure;
canvas_width = 1200;
canvas_height = 800;

% generation parameters
base_sin_period = 1.0 * canvas_width; % controls the number of summits
base_amp = 0.14 * canvas_height; % controls altitude variation
base_noise_var = 4; % controls the noise strength
bm_ratio = 0.15; % controls the amplitude of the Brownian motion 
decay = 0.8; % controls the decaying factor for farther mountains
n_mountain = 6; % # of mountains
% color configurations
mountain_color = [216 171 132] / 255;
sun_center_color = [254 243 105] / 255;
sun_edge_color = [248 130 6] / 255;
sky_color = [239 185 127] / 255;

end

Adding the drawing logic

The drawing logic is quite simple. We just need to draw layer by layer:

  1. Draw the background/sky using rectangle.
  2. Draw the mountains using fill, from the farthest one to the nearest one, with generation parameters tweaked according to the distance.
  3. Draw the sunlight using patch.

The corresponding code is given below:

% draw the background (sky)
rectangle('Position', [0 0 canvas_width canvas_height], ...
    'EdgeColor', 'none', 'FaceColor', sky_color); hold on;
% draw the mountains, from the farthest one to the nearest one
for ii = n_mountain:-1:1
    y = ii / (n_mountain + 3) * canvas_height;
    % give lighter colors for farther mountains
    color = ii / n_mountain / 1.5 * mountain_color;
    % reduce the amount of superimposed Brownian motion and noise for father
    % mountains
    amp = base_amp * decay^(ii - 1);
    noise_var = base_noise_var * decay^(ii - 1);
    % give more summits to farther mountains
    sin_period = base_sin_period * (1.0 - (ii - 1) / n_mountain);
    draw_mountain_range(y, sin_period, amp, noise_var, 800, color);
end
% draw the sunlight
draw_sunlight(sun_center_color, sun_edge_color, 0.03);
% update axis config
axis('equal');
axis([0 canvas_width 0 canvas_height]);

Implementing draw_mountain_range

We mimic the outline of a mountain by the superposition of the following four components:

  1. A sine wave as the base shape.
  2. Another sine wave with longer period to give a slight variation over the amplitudes, because a pure sine wave looks to dull and artificial.
  3. A sample path of the 1D Brownian motion for more variations and roughness.
  4. White Gaussian noise for additional roughness.

The following gif summarizes the whole procedure:

outline generation

With the outline generated, we can close the drawing path and call fill to draw the mountain. The implementation of draw_mountain_range is given below:

function draw_mountain_range(y, sin_period, amp, noise_var, n, color)
x_grid = linspace(0, canvas_width, n);
phase_offset = y / canvas_height * 4 * pi;
thetas = x_grid * 2 * pi / sin_period;
% use the sum of two sine functions as the base curve
base = sin(phase_offset + thetas);
base = base + sin(phase_offset + 0.3 * thetas);
base = base * amp;
% add Brownian motion
bm = cumsum(sqrt(bm_ratio * amp) * randn(1, n));
% add Gaussian noise
final_curve = y + base + bm + noise_var * randn(1, n);
% prepare vertices for the fill functions
vertices = [...
    0 0;...
    x_grid' final_curve';...
    canvas_width 0;...
    0 0];
% draw the current mountain
fill(vertices(:,1), vertices(:,2), color, 'EdgeColor', 'none');
end

Implementing draw_sunlight

To draw the sunlight overlay, we need to use the patch function, whose detailed documentation is given here. In fact, batch is a quite "low-level" drawing command in MATLAB as you need to decompose the shape you want to draw into triangles/polygons, specify each vertex and face, as well as their color and alpha. To get the gradient fill, FaceColor and FaceAlpha need to be set to interp, and we need to manually specify the color and alpha values for each vertex. We also need to set AlphaDataMapping to none because we are using absolute alpha values.

In our scenario, the sun is located on the top left corner of the figure. Hence we can mimic the sunlight by drawing a quarter disc filled using radial gradient. The triangulation of a quarter disc is illustrated on the left side of the following figure. To make it nicer looking, we mimic the light beams by adding some randomness to the length of the shared edges of these triangles, as shown on the right side of the following figure.

sun rays how to

The complete implementation of draw_sunlight is give below:

function draw_sunlight(c_color, e_color, randomness)
n_tri = 20; % number of triangles
theta = linspace(-pi / 2, 0, n_tri + 1)';
diag_length = sqrt(canvas_height^2 + canvas_width^2);
% adding some randomness to the radius to mimic the effect of sunlight
radius = diag_length * (1.0 + max(min(randomness * randn(n_tri + 1, 1), 1), -1));
% building the drawing parameters for patch
S.EdgeColor = 'none';
S.FaceColor = 'interp';
S.FaceAlpha = 'interp';
S.AlphaDataMapping = 'none';
S.Vertices = [...
    0 canvas_height;...
    radius .* cos(theta) canvas_height + radius .* sin(theta)];
S.Faces = [ones(n_tri, 1) (3:(n_tri + 2))' (2:(n_tri + 1))'];
S.FaceVertexCData = [c_color; repmat(e_color, n_tri + 1, 1)];
S.FaceVertexAlphaData = [1; 0.0*ones(n_tri + 1, 1)];
patch(S);
end

Putting everything together

Now we can put everything together and finalize our function:

function plot_mountains
%PLOT_MOUNTAINS Plots mountains.

% set up figure
figure;
canvas_width = 1200;
canvas_height = 800;

% generation parameters
base_sin_period = 1.0 * canvas_width; % controls the number of summits
base_amp = 0.25 * canvas_height; % controls altitude variation
base_noise_var = 4; % controls the noise strength
bm_ratio = 0.15; % controls the amplitude of the Brownian motion 
decay = 0.8; % controls the decaying factor for farther mountains
n_mountain = 6; % # of mountains
% color configurations
mountain_color = [216 171 132] / 255;
sun_center_color = [254 243 105] / 255;
sun_edge_color = [248 130 6] / 255;
sky_color = [239 185 127] / 255;

% draw the background (sky)
rectangle('Position', [0 0 canvas_width canvas_height], ...
    'EdgeColor', 'none', 'FaceColor', sky_color); hold on;
% draw the mountains, from the farthest one to the nearest one
for ii = n_mountain:-1:1
    y = ii / (n_mountain + 3) * canvas_height;
    % give lighter colors for farther mountains
    color = ii / n_mountain / 1.5 * mountain_color;
    % reduce the amount of superimposed Brownian motion and noise for father
    % mountains
    amp = base_amp * decay^(ii - 1);
    noise_var = base_noise_var * decay^(ii - 1);
    % give more summits to farther mountains
    sin_period = base_sin_period * (1.0 - (ii - 1) / n_mountain);
    draw_mountain_range(y, sin_period, amp, noise_var, 800, color);
end
% draw the sunlight
draw_sunlight(sun_center_color, sun_edge_color, 0.03);
% update axis config
axis('equal');
axis([0 canvas_width 0 canvas_height]);

% nested functions
function draw_mountain_range(y, sin_period, amp, noise_var, n, color)
x_grid = linspace(0, canvas_width, n);
phase_offset = y / canvas_height * 4 * pi;
thetas = x_grid * 2 * pi / sin_period;
% use the sum of two sine functions as the base curve
base = sin(phase_offset + thetas);
base = base + sin(phase_offset + 0.3 * thetas);
base = base * amp;
% add Brownian motion
bm = cumsum(sqrt(bm_ratio * amp) * randn(1, n));
% add Gaussian noise
final_curve = y + base + bm + noise_var * randn(1, n);
% prepare vertices for the fill functions
vertices = [...
    0 0;...
    x_grid' final_curve';...
    canvas_width 0;...
    0 0];
% draw the current mountain
fill(vertices(:,1), vertices(:,2), color, 'EdgeColor', 'none');
end

function draw_sunlight(c_color, e_color, randomness)
n_tri = 20; % number of triangles
theta = linspace(-pi / 2, 0, n_tri + 1)';
diag_length = sqrt(canvas_height^2 + canvas_width^2);
% adding some randomness to the radius to mimic the effect of sunlight
radius = diag_length * (1.0 + max(min(randomness * randn(n_tri + 1, 1), 1), -1));
% building the drawing parameters for patch
S.EdgeColor = 'none';
S.FaceColor = 'interp';
S.FaceAlpha = 'interp';
S.AlphaDataMapping = 'none';
S.Vertices = [...
    0 canvas_height;...
    radius .* cos(theta) canvas_height + radius .* sin(theta)];
S.Faces = [ones(n_tri, 1) (3:(n_tri + 2))' (2:(n_tri + 1))'];
S.FaceVertexCData = [c_color; repmat(e_color, n_tri + 1, 1)];
S.FaceVertexAlphaData = [1; 0.0*ones(n_tri + 1, 1)];
patch(S);
end

end

The parameters can be tweaked to obtain different looking mountains. If we increase n_mountain to 12, we will get more depth:

mountain 12

If we increase base_amp, we will get steeper looking mountains:

mountain more variation