From 8d4c0067a5fecb4e4e05dade1c81167b60be235f Mon Sep 17 00:00:00 2001 From: Elias Fleckenstein Date: Tue, 18 Aug 2020 17:52:36 +0200 Subject: [PATCH] Dynamic World System --- README | 8 +- init.lua | 3 +- luac.out | Bin 0 -> 776 bytes modules/Client/src/graphics.lua | 20 ++-- modules/Client/src/init.lua | 9 +- modules/MapGen/src/init.lua | 52 ++++----- modules/PlayerSystem/src/localplayer.lua | 8 ++ modules/RenderEngine/src/chunk_mesh.lua | 15 ++- modules/RenderEngine/src/init.lua | 9 +- modules/WorldSystem/src/block.lua | 4 +- modules/WorldSystem/src/chunk.lua | 46 +++++--- modules/WorldSystem/src/map.lua | 69 +++++++++++- src/class.lua | 2 +- src/init.lua | 1 + src/modulemgr.lua | 2 +- src/taskmgr.lua | 6 +- src/timeout.lua | 38 +++++++ util/perlin.lua | 129 +++++++++++++++++++++++ 18 files changed, 341 insertions(+), 80 deletions(-) create mode 100755 luac.out create mode 100644 src/timeout.lua create mode 100644 util/perlin.lua diff --git a/README b/README index 1115293..8701704 100644 --- a/README +++ b/README @@ -1,7 +1,11 @@ depends: -$ sudo apt install lua5.3 libsqlite3 + +$ sudo apt install build-essential make lua5.3 liblua5.3-dev libgl1-mesa-dev libglew-dev libglm-dev libglfw3-dev libassimp-dev libsqlite3-dev luarocks $ luarocks install lsqlite3 $ luarocks install luasocket $ luarocks install luafilesystem -Build moongl, moonglfw, moonglmath, moonimage and moonassimp: Only needed for running the client, see build instructions in deps/*/README.md +Build moongl, moonglfw, moonglmath, moonimage and moonassimp: See build instructions in deps/*/README.md + +Run: + ./init.lua Client diff --git a/init.lua b/init.lua index da40c8a..ddb2d15 100755 --- a/init.lua +++ b/init.lua @@ -4,8 +4,9 @@ socket = require("socket") lsqlite3 = require("lsqlite3") gl = require("moongl") glfw = require("moonglfw") -image = require("moonimage") glm = require("moonglmath") +image = require("moonimage") +perlin = require("util/perlin") string.split = require("util/string_split") table.indexof = require("util/table_indexof") table.assign = require("util/table_assign") diff --git a/luac.out b/luac.out new file mode 100755 index 0000000000000000000000000000000000000000..260066264c3817f76f29cc7539a667324c1a489b GIT binary patch literal 776 zcmZ9K%TB^T6oyaX^g@A{=nCBu<4O`;xEi zfLwWA`9}G+^jtf$%JDC{UiqtXRx9gox?cIauGh=@hm+sHJ)L0#2xB^f&+<=)-Rf*x zX7E*I%r3^kw_e*W*ZNTo4s;n1_1OGi=CoAw|Y7u^viv zquDH`)(+)lg$-@g!%#-p)EWiZ6de%-AxD1~>8?BHi4-xmmYI;fLfkY!`%&S?`o zR5hIqbmU~gP;&G{Jm%}!1dr4Z6%NYG4boJEaWP%jipZPt^+gg6v90En)<~04Z}-p^ z-FyeY?u~1E)X7bd+{7j~amYuSW&5-#GdUnPAyqfE2X~MC EKaBT*;{X5v literal 0 HcmV?d00001 diff --git a/modules/Client/src/graphics.lua b/modules/Client/src/graphics.lua index a56074b..2910355 100644 --- a/modules/Client/src/graphics.lua +++ b/modules/Client/src/graphics.lua @@ -4,12 +4,12 @@ function graphics:init() RenderEngine:init() RenderEngine.bininear_filter = false - RenderEngine.mipmap = false + RenderEngine.mipmap = true RenderEngine.mouse_sensitivity = 0.7 - --RenderEngine.pitch_move = true + RenderEngine.pitch_move = false RenderEngine.mesh_effect_grow_time = 0.25 - RenderEngine.mesh_effect_flyin_time = 0.5 - RenderEngine.mesh_effect_flyin_offset = 20 + RenderEngine.mesh_effect_flyin_time = 0.25 + RenderEngine.mesh_effect_flyin_offset = 10 RenderEngine.mesh_effect_rotate_speed = 1 RenderEngine:set_wireframe(false) @@ -20,17 +20,9 @@ function graphics:init() RenderEngine:set_sky("#87CEEB") + RenderEngine:toggle_fullscreen() + BlockSystem:init_textures() end -function graphics:create_chunk_meshes(chunk) - local mesh = RenderEngine.ChunkMesh() - mesh:set_pos(glm.vec3(0, 0, 0)) - mesh:set_size(glm.vec3(1, 1, 1)) - mesh:set_texture(BlockSystem:get_def("game:dirt").texture) - mesh:create_vertices(chunk) - mesh:set_effect(RenderEngine.Mesh.EFFECT_FLYIN) - mesh:add_to_scene() -end - return graphics diff --git a/modules/Client/src/init.lua b/modules/Client/src/init.lua index c368a2d..1096f9f 100644 --- a/modules/Client/src/init.lua +++ b/modules/Client/src/init.lua @@ -6,13 +6,14 @@ PlayerSystem:init("client") Client.map = WorldSystem.Map() Client.player = PlayerSystem.LocalPlayer() -Client.player:set_position(glm.vec3(8, 20, 8)) +Client.player:set_position(glm.vec3(8, 8, 8)) + Dragonblocks:add_task(function() - repeat + while true do coroutine.yield("FPS:" .. math.floor(Dragonblocks.tps or 0)) - until false + end end) -RenderEngine:render_loop(true) +--RenderEngine:render_loop(true) diff --git a/modules/MapGen/src/init.lua b/modules/MapGen/src/init.lua index 5d2c4cb..21a0a3f 100644 --- a/modules/MapGen/src/init.lua +++ b/modules/MapGen/src/init.lua @@ -4,42 +4,32 @@ local grass = BlockSystem:get_def("game:grass") local leaves = BlockSystem:get_def("game:leaves") local tree = BlockSystem:get_def("game:tree") -math.randomseed(os.time()) +local grass_layer_start, grass_layer_end = 0, 30 +local grass_layer_height = grass_layer_end - grass_layer_start -function MapGen:generate(chunk) - local grass_layer_table, old_grass_layer_table - local grass_layer - for x = 0, 15 do - grass_layer_table, old_grass_layer_table = {}, grass_layer_table - grass_layer = old_grass_layer_table and old_grass_layer_table[1] or 8 + math.random(5) - for z = 0, 15 do - local old_grass_layer = old_grass_layer_table and old_grass_layer_table[z] or grass_layer - grass_layer = math.floor((grass_layer + old_grass_layer) / 2) - if math.random(3) == 1 then - grass_layer = grass_layer + math.random(3) - 2 - end - grass_layer = glm.clamp(grass_layer, 0, 15) - grass_layer_table[z] = grass_layer - if math.random(25) == 1 then - chunk:add_block(glm.vec3(x, grass_layer, z), dirt) - self:add_tree(chunk, glm.vec3(x, grass_layer + 1, z)) - else - chunk:add_block(glm.vec3(x, grass_layer, z), grass) - end - local dirt_start, dirt_end = grass_layer - 1, math.max(grass_layer - 5, 0) - local stone_start, stone_end = grass_layer - 6, 0 - if dirt_start >= 0 then - for y = dirt_start, dirt_end, -1 do - chunk:add_block(glm.vec3(x, y, z), dirt) - end - end - if stone_start >= 0 then - for y = stone_start, stone_end, -1 do - chunk:add_block(glm.vec3(x, y, z), stone) +function MapGen.generate(minp, maxp) + local data = {} + local minx, miny, minz, maxx, maxy, maxz = minp.x, minp.y, minp.z, maxp.x - 1, maxp.y - 1, maxp.z - 1 + for x = minx, maxx do + for z = minz, maxz do + local grass_layer = math.floor(grass_layer_start + grass_layer_height * perlin:noise(x / grass_layer_height, z / grass_layer_height)) + for y = miny, maxy do + local pos = glm.vec3(x - minx, y - miny, z - minz) + local block + if y <= grass_layer - 5 then + block = stone + elseif y <= grass_layer - 1 then + block = dirt + elseif y <= grass_layer then + block = grass + end + if block then + data[WorldSystem.Chunk.get_pos_hash(pos)] = WorldSystem.Block(pos, block) end end end end + return data end local tree_blocks = { diff --git a/modules/PlayerSystem/src/localplayer.lua b/modules/PlayerSystem/src/localplayer.lua index 753520f..c5f4c28 100644 --- a/modules/PlayerSystem/src/localplayer.lua +++ b/modules/PlayerSystem/src/localplayer.lua @@ -52,6 +52,14 @@ end function LocalPlayer:set_position_callback(event) RenderEngine.camera.pos = self.pos + local pos = WorldSystem.Map.get_chunk_pos(self.pos) + for x = pos.x - 1, pos.x + 1 do + for y = pos.y - 1, pos.y + 1 do + for z = pos.z - 1, pos.z + 1 do + Client.map:create_chunk_if_not_exists(glm.vec3(x, y, z)) + end + end + end end function LocalPlayer:move(vec) diff --git a/modules/RenderEngine/src/chunk_mesh.lua b/modules/RenderEngine/src/chunk_mesh.lua index 4a1369d..29a092e 100644 --- a/modules/RenderEngine/src/chunk_mesh.lua +++ b/modules/RenderEngine/src/chunk_mesh.lua @@ -1,7 +1,7 @@ local ChunkMesh = Dragonblocks.create_class() table.assign(ChunkMesh, RenderEngine.Mesh) -function ChunkMesh:create_vertices(chunk) +function ChunkMesh:create_faces(blocks) self.vertices = {} self.textures = {} self.vertex_blob_size = 6 @@ -13,14 +13,21 @@ function ChunkMesh:create_vertices(chunk) glm.vec3( 0, -1, 0), glm.vec3( 0, 1, 0), } - for _, block in pairs(chunk.blocks) do + local bc = 0 + for _, block in pairs(blocks) do for i, dir in ipairs(face_orientations) do local pos = block.pos - if not chunk:get_block(pos + dir) then + local dir_pos_hash = WorldSystem.Chunk.get_pos_hash(pos + dir) + if not dir_pos_hash or not blocks[dir_pos_hash] then table.insert(self.textures, block.def.texture) - self:add_face(block.pos, i) + self:add_face(block.pos - glm.vec3(7.5, 7.5, 7.5), i) end end + bc = bc + 1 + if bc == 64 then + bc = 0 + coroutine.yield() + end end self:apply_vertices(self.vertices) end diff --git a/modules/RenderEngine/src/init.lua b/modules/RenderEngine/src/init.lua index 4b060ab..ca7a9fa 100644 --- a/modules/RenderEngine/src/init.lua +++ b/modules/RenderEngine/src/init.lua @@ -39,7 +39,7 @@ function RenderEngine:render_loop(is_only_task) end function RenderEngine:update_projection_matrix() - gl.uniform_matrix4f(gl.get_uniform_location(self.shaders, "projection"), true, glm.perspective(math.rad(self.fov), self.window_width / self.window_height, 0.0001, 100)) + gl.uniform_matrix4f(gl.get_uniform_location(self.shaders, "projection"), true, glm.perspective(math.rad(self.fov), self.window_width / self.window_height, 0.01, 100)) end function RenderEngine:update_view_matrix() @@ -76,3 +76,10 @@ end function RenderEngine:set_wireframe(v) gl.polygon_mode("front and back", (v and "line" or "fill")) end + +function RenderEngine:toggle_fullscreen() + self.fullscreen = not self.fullscreen + local monitor = glfw.get_primary_monitor() + local mode = glfw.get_video_mode(monitor) + glfw.set_window_monitor(self.window, self.fullscreen and monitor, 0, 0, mode.width, mode.height, 0) +end diff --git a/modules/WorldSystem/src/block.lua b/modules/WorldSystem/src/block.lua index 2c67a27..b963c94 100644 --- a/modules/WorldSystem/src/block.lua +++ b/modules/WorldSystem/src/block.lua @@ -1,7 +1,7 @@ local Block = Dragonblocks.create_class() -function Block:constructor(def, pos) - self.def, self.pos = def, pos +function Block:constructor(pos, def) + self.pos, self.def = pos, def end return Block diff --git a/modules/WorldSystem/src/chunk.lua b/modules/WorldSystem/src/chunk.lua index f49390e..96eeef4 100644 --- a/modules/WorldSystem/src/chunk.lua +++ b/modules/WorldSystem/src/chunk.lua @@ -3,31 +3,51 @@ local Chunk = Dragonblocks.create_class() local size = 16 local size_squared = math.pow(size, 2) -function Chunk:constructor() - self.blocks = {} - MapGen:generate(self) - if Client then - Client.graphics:create_chunk_meshes(self) - end -end - -function Chunk:get_pos_hash(pos) +function Chunk.get_pos_hash(pos) local x, y, z = pos.x, pos.y, pos.z if x > 15 or y > 15 or z > 15 or x < 0 or y < 0 or z < 0 then return end return x + size * y + size_squared * z end +function Chunk:constructor(pos, blocks) + self.pos, self.blocks = pos, blocks +end + function Chunk:add_block(pos, def) - local block = WorldSystem.Block(def, pos) - self.blocks[self:get_pos_hash(pos)] = block + local pos_hash = Chunk.get_pos_hash(pos) + if pos_hash then + self.blocks[pos_hash] = WorldSystem.Block(pos, def) + self:update_mesh() + end end function Chunk:remove_block(pos) - self.blocks[self:get_pos_hash(pos)] = nil + local pos_hash = Chunk.get_pos_hash(pos) + if pos_hash then + self.blocks[pos_hash] = nil + self:update_mesh() + end end function Chunk:get_block(pos) - return self.blocks[self:get_pos_hash(pos)] + local pos_hash = Chunk.get_pos_hash(pos) + if pos_hash then return self.blocks[pos_hash] end +end + +function Chunk:update_mesh() + if #self.blocks == 0 then return end + local mesh = RenderEngine.ChunkMesh() + mesh:set_pos(self.pos * 16 + glm.vec3(8, 8, 8)) + mesh:set_size(glm.vec3(1, 1, 1)) + mesh:create_faces(self.blocks) + if not self.mesh then + mesh:set_effect(RenderEngine.Mesh.EFFECT_FLYIN) + end + if self.mesh then + self.mesh:remove_from_scene() + end + self.mesh = mesh + mesh:add_to_scene() end return Chunk diff --git a/modules/WorldSystem/src/map.lua b/modules/WorldSystem/src/map.lua index 44d7490..b253f27 100644 --- a/modules/WorldSystem/src/map.lua +++ b/modules/WorldSystem/src/map.lua @@ -1,7 +1,74 @@ local Map = Dragonblocks.create_class() +local size = 1000 +local size_squared = math.pow(size, 2) + + +function Map.get_pos_hash(pos) + local x, y, z = pos.x, pos.y, pos.z + if x > 999 or y > 999 or z > 999 or x < -999 or y < -999 or z < -999 then return end + return x + size * y + size_squared * z +end + +function Map.get_chunk_pos(pos) + return glm.vec3(math.floor(pos.x / 16), math.floor(pos.y / 16), math.floor(pos.z / 16)) +end + +function Map.get_block_pos(pos) + return pos * 16 +end + +function Map.get_chunk_pos_and_block_pos(pos) + local chunk_pos = Map.get_chunk_pos(pos) + local block_pos = pos - Map.get_block_pos(chunk_pos) + return chunk_pos, block_pos +end + function Map:constructor() - self.chunk = WorldSystem.Chunk() + self.chunks = {} +end + +function Map:get_block(pos) + local chunk, block_pos = self:get_chunk_and_block_pos(pos) + if chunk then return chunk:get_block(block_pos) end +end + +function Map:add_block(pos, block) + local chunk, block_pos = self:get_chunk_and_block_pos(pos) + if chunk then return chunk:add_block(block_pos, block) end +end + +function Map:remove_block(pos) + local chunk, block_pos = self:get_chunk_and_block_pos(pos) + if chunk then return chunk:remove_block(block_pos) end +end + +function Map:create_chunk(pos, data) + local pos_hash = Map.get_pos_hash(pos) + if not pos_hash then return end + local minp = Map.get_block_pos(pos) + local maxp = minp + glm.vec3(16, 16, 16) + local data = data or MapGen.generate(minp, maxp) + local chunk = WorldSystem.Chunk(pos, data) + self.chunks[pos_hash] = chunk + Dragonblocks:add_task(function() + chunk:update_mesh() + end) +end + +function Map:create_chunk_if_not_exists(pos, data) + if not self:get_chunk(pos) then self:create_chunk(pos, data) end +end + +function Map:get_chunk(pos) + local pos_hash = Map.get_pos_hash(pos) + if pos_hash then return self.chunks[pos_hash] end +end + +function Map:get_chunk_and_block_pos(pos) + local chunk_pos, block_pos = Map.get_chunk_pos_and_block_pos(pos) + local chunk = self:get_chunk(chunk_pos) + return chunk, block_pos end return Map diff --git a/src/class.lua b/src/class.lua index 5b2c20f..470e5cc 100644 --- a/src/class.lua +++ b/src/class.lua @@ -1,4 +1,4 @@ -function Dragonblocks.create_class() +function Dragonblocks:create_class() local class = self or {} setmetatable(class, { __call = function(_, ...) diff --git a/src/init.lua b/src/init.lua index b94e151..60c6bd5 100644 --- a/src/init.lua +++ b/src/init.lua @@ -5,6 +5,7 @@ require("src/events") require("src/taskmgr") require("src/modulemgr") require("src/serialisation") +require("src/timeout") print("Started Dragonblocks core") diff --git a/src/modulemgr.lua b/src/modulemgr.lua index 7aaa1cf..b102f31 100644 --- a/src/modulemgr.lua +++ b/src/modulemgr.lua @@ -37,7 +37,7 @@ end function Dragonblocks:read_modules() if not lfs.attributes("data", "mode") then - lfs.mkdir(self.data_path) + lfs.mkdir("data") end self.modules = {} for modulename in lfs.dir("modules") do diff --git a/src/taskmgr.lua b/src/taskmgr.lua index 4c4693e..91e5aef 100644 --- a/src/taskmgr.lua +++ b/src/taskmgr.lua @@ -11,11 +11,7 @@ function Dragonblocks:step() local tasks = self.tasks self.tasks = {} for _, t in ipairs(tasks) do - local continue, status = coroutine.resume(t) - if status then - print(status) - end - if continue then + if coroutine.status(t) ~= "dead" and coroutine.resume(t) then table.insert(self.tasks, t) end end diff --git a/src/timeout.lua b/src/timeout.lua new file mode 100644 index 0000000..527b1d9 --- /dev/null +++ b/src/timeout.lua @@ -0,0 +1,38 @@ +local timeout = Dragonblocks.create_class() + +timeout.list = {} + +function timeout:constructor(sec, func, ...) + self.exp, self.func, self.args = socket.gettime() + sec, func, table.pack(...) + table.insert(timeout.list, self) +end + +function timeout:clear() + self.cleared = true +end + +function Dragonblocks.set_timeout(sec, func, ...) + return timeout(sec, func, ...) +end + +function Dragonblocks:clear_timeout() + self:clear() +end + +Dragonblocks:add_task(function() + while true do + local tolist = timeout.list + local tm = socket.gettime() + timeout.list = {} + for _, to in pairs(tolist) do + if not to.cleared then + if to.exp <= tm then + to.func(table.unpack(to.args)) + else + table.insert(timeout.list, to) + end + end + end + coroutine.yield() + end +end) diff --git a/util/perlin.lua b/util/perlin.lua new file mode 100644 index 0000000..e9f84eb --- /dev/null +++ b/util/perlin.lua @@ -0,0 +1,129 @@ +--[[ + Implemented as described here: + http://flafla2.github.io/2014/08/09/perlinnoise.html +]]-- + +local perlin = {} +perlin.p = {} + +-- Hash lookup table as defined by Ken Perlin +-- This is a randomly arranged array of all numbers from 0-255 inclusive +local permutation = {151,160,137,91,90,15, + 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, + 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, + 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, + 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, + 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, + 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, + 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, + 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, + 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, + 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, + 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, + 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 +} + +-- p is used to hash unit cube coordinates to [0, 255] +for i=0,255 do + -- Convert to 0 based index table + perlin.p[i] = permutation[i+1] + -- Repeat the array to avoid buffer overflow in hash function + perlin.p[i+256] = permutation[i+1] +end + +-- Return range: [-1, 1] +function perlin:noise(x, y, z) + y = y or 0 + z = z or 0 + + -- Calculate the "unit cube" that the point asked will be located in + local xi = bit32.band(math.floor(x),255) + local yi = bit32.band(math.floor(y),255) + local zi = bit32.band(math.floor(z),255) + + -- Next we calculate the location (from 0 to 1) in that cube + x = x - math.floor(x) + y = y - math.floor(y) + z = z - math.floor(z) + + -- We also fade the location to smooth the result + local u = self.fade(x) + local v = self.fade(y) + local w = self.fade(z) + + -- Hash all 8 unit cube coordinates surrounding input coordinate + local p = self.p + local A, AA, AB, AAA, ABA, AAB, ABB, B, BA, BB, BAA, BBA, BAB, BBB + A = p[xi ] + yi + AA = p[A ] + zi + AB = p[A+1 ] + zi + AAA = p[ AA ] + ABA = p[ AB ] + AAB = p[ AA+1 ] + ABB = p[ AB+1 ] + + B = p[xi+1] + yi + BA = p[B ] + zi + BB = p[B+1 ] + zi + BAA = p[ BA ] + BBA = p[ BB ] + BAB = p[ BA+1 ] + BBB = p[ BB+1 ] + + -- Take the weighted average between all 8 unit cube coordinates + return self.lerp(w, + self.lerp(v, + self.lerp(u, + self:grad(AAA,x,y,z), + self:grad(BAA,x-1,y,z) + ), + self.lerp(u, + self:grad(ABA,x,y-1,z), + self:grad(BBA,x-1,y-1,z) + ) + ), + self.lerp(v, + self.lerp(u, + self:grad(AAB,x,y,z-1), self:grad(BAB,x-1,y,z-1) + ), + self.lerp(u, + self:grad(ABB,x,y-1,z-1), self:grad(BBB,x-1,y-1,z-1) + ) + ) + ) +end + +-- Gradient function finds dot product between pseudorandom gradient vector +-- and the vector from input coordinate to a unit cube vertex +perlin.dot_product = { + [0x0]=function(x,y,z) return x + y end, + [0x1]=function(x,y,z) return -x + y end, + [0x2]=function(x,y,z) return x - y end, + [0x3]=function(x,y,z) return -x - y end, + [0x4]=function(x,y,z) return x + z end, + [0x5]=function(x,y,z) return -x + z end, + [0x6]=function(x,y,z) return x - z end, + [0x7]=function(x,y,z) return -x - z end, + [0x8]=function(x,y,z) return y + z end, + [0x9]=function(x,y,z) return -y + z end, + [0xA]=function(x,y,z) return y - z end, + [0xB]=function(x,y,z) return -y - z end, + [0xC]=function(x,y,z) return y + x end, + [0xD]=function(x,y,z) return -y + z end, + [0xE]=function(x,y,z) return y - x end, + [0xF]=function(x,y,z) return -y - z end +} +function perlin:grad(hash, x, y, z) + return self.dot_product[bit32.band(hash,0xF)](x,y,z) +end + +-- Fade function is used to smooth final output +function perlin.fade(t) + return t * t * t * (t * (t * 6 - 15) + 10) +end + +function perlin.lerp(t, a, b) + return a + t * (b - a) +end + +return perlin -- 2.44.0