#!/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")