#!/usr/bin/env lua5.1
-- -*- coding: utf-8 -*-
-- fractal mountains
--
-- cargo-culted by erle 2023-11-04
local screen_height = 256
local screen_width = 512
function vec3_add( a, b )
-- assert( "table" == type(a) )
-- assert( "table" == type(b) )
return {
x = a.x + b.x,
y = a.y + b.y,
z = a.z + b.z,
}
end
function vec3_dot( a, b )
-- assert( "table" == type(a) )
-- assert( "table" == type(b) )
return a.x * b.x + a.y * b.y + a.z * b.z
end
function vec3_len( a )
-- assert( "table" == type(a) )
return math.sqrt(
math.pow( a.x, 2 ) +
math.pow( a.y, 2 ) +
math.pow( a.z, 2 )
)
end
function vec3_mul( a, n )
-- assert( "table" == type(a) )
-- assert( "number" == type(n) )
return {
x = a.x * n,
y = a.y * n,
z = a.z * n,
}
end
function vec3_norm( a )
-- assert( "table" == type(a) )
local len = vec3_len( a )
return {
x = a.x / len,
y = a.y / len,
z = a.z / len,
}
end
function f( x, z )
local terrain_height =
math.atan(x / z) +
math.sin(x / 8) * math.cos(z / 8) +
math.sin(x / 4) * math.cos(z / 4) +
math.sin(x / 2) * math.cos(z / 2) +
math.sin(x) * math.cos(z) / 4 +
math.sin(2 * x) * math.cos(2 * z) / 8 +
math.sin(4 * x) * math.cos(4 * z) / 16
if x < -8 then
terrain_height = terrain_height + ( math.abs( x + 8 ) / 4 )
end
if x > 8 then
terrain_height = terrain_height + ( math.abs( x - 8 ) / 4 )
end
if z > 32 and z < 48 and x > -4 and x < 4 then
return 1.5 + x/2
end
if z > 32 and z < 48 and x > -8 and x < 8 then
return z/8
end
if z > 32 then
terrain_height = terrain_height * math.log(z / math.abs(x)) / math.log(2)
end
return terrain_height
end
local base_d_t = 0.125
local base_max_t = 256
function cast_ray( vec3_ro, vec3_rd )
local min_t = screen_width / 32 -- distance to near clipping plane
local max_t = base_max_t -- distance that rays can travel
local d_t = base_d_t -- step size (large → fast rendering)
if vec3_rd.x < -0.1 or vec3_rd.x > 0.1 then
d_t = base_d_t * 2
end
if vec3_rd.x < -0.2 or vec3_rd.x > 0.2 then
d_t = base_d_t * 4
end
if vec3_rd.x < -0.285 or vec3_rd.x > 0.285 then
d_t = base_d_t * 8
end
for t = min_t,max_t,d_t do
local vec3_p = vec3_add( vec3_ro, ( vec3_mul( vec3_rd, t ) ) )
local terrain_height = f( vec3_p.x, vec3_p.z )
if ( vec3_p.y < terrain_height ) then
local res_t = t - ( 0.5 * d_t )
return { true, res_t, terrain_height }
end
end
return { false, max_t, 0 }
end
function get_normal( vec3_p )
local eps = 0.1
return vec3_norm(
{
x = f( vec3_p.x - eps, vec3_p.z ) - f( vec3_p.x + eps, vec3_p.z ),
y = 2 * eps,
z = f( vec3_p.x, vec3_p.z - eps ) - f( vec3_p.x, vec3_p.z + eps ),
}
)
end
function lerp( a, b, t )
return math.floor(a + (b - a) * math.abs(t))
end
function lerp_color( r, g, b )
return {
lerp( 0, 255, r ),
lerp( 0, 255, g ),
lerp( 0, 255, b ),
}
end
function clamp(x, min, max)
return math.min( math.max ( x, min ), max )
end
function terrain_color( ray, t, terrain_height )
local brightness = clamp( terrain_height / 16, 0, 1 )
if ray.direction.x < 0 and ray.direction.y >= 0 then
-- top left: return terrain height
return lerp_color ( brightness, brightness, brightness )
end
vec3_p = vec3_add( ray.origin, ( vec3_mul( ray.direction, t ) ) )
vec3_n = get_normal( vec3_p )
if ray.direction.x < 0 and ray.direction.y < 0 then
-- bottom left: return normal
return lerp_color ( vec3_n.x, vec3_n.y, vec3_n.z )
end
local vec3_l = {
x = 0.3,
y = 0.4,
z = -.5,
}
local sunlight = clamp( vec3_dot( vec3_n, vec3_l ), 0, 1 )
if ray.direction.x >= 0 and ray.direction.y < 0 then
-- bottom right: return lightmap
return lerp_color ( sunlight, sunlight, sunlight )
end
if ray.direction.x >= 0 and ray.direction.y >= 0 then
-- top right: return lighted surface
local l = clamp( sunlight * brightness * 2, 0, 1 )
return lerp_color ( 0, l, 0 )
end
end
function sky_color( x, y )
return {
lerp( 0, 255, ( x - 1 ) / screen_width ),
lerp( 0, 255, y / screen_height ),
lerp( 0, 255, 1 - (x / screen_width ) ),
}
end
function apply_fog( color, distance )
local fog_density = clamp( math.pow( distance / base_max_t, 2 ), 0, 0.75 )
-- assert(fog_density <= 1.0)
fog_color = {
255, 255, 191
}
return {
math.floor(color[1] * (1 - fog_density) + fog_density * fog_color[1]),
math.floor(color[2] * (1 - fog_density) + fog_density * fog_color[2]),
math.floor(color[3] * (1 - fog_density) + fog_density * fog_color[3]),
}
end
--
function generate_ray( x, y )
local fov = 75
local ray = {}
ray.origin = {
x = 0.0,
y = 3.0,
z = 0.0,
}
ray.direction = vec3_norm(
{
x = x - ( screen_width / 2 ),
y = ( screen_height / 2 ) - y,
z = -( screen_height / 2 ) / math.tan( fov * 0.5 ),
}
)
return ray
end
local output = {}
function render_frame()
for y = 1,screen_height,1 do
for x = 1,screen_width,1 do
local ray = generate_ray( x, y )
local result = cast_ray( ray.origin, ray.direction )
local color
local distance = result[2]
local height = result[3]
if result[1] then
color = terrain_color( ray, distance, height )
else
color = sky_color( x, y )
end
if (
y - 1 < math.ceil(3*screen_height/4) and
y - 1 > math.ceil(screen_height/4) and
x < math.ceil(7*screen_width/8) and
x > math.ceil(screen_width/8)
) then
-- fog in the middle of the picture
color = apply_fog( color, distance )
end
output[screen_height+1-y] = output[screen_height+1-y] or {}
output[screen_height+1-y][x] = color
end
end
end
dofile('init.lua')
render_frame()
tga_encoder.image(output):save("terrain.tga")