diff --git a/main.lua b/main.lua index 034a114..cdd8579 100644 --- a/main.lua +++ b/main.lua @@ -21,30 +21,30 @@ end local BIG_BLOCK = love.graphics.newQuad(24 * 9, 24 * 9, 24, 24, tex) -local z = require("zprite").new(tex, 262144) -z._height_scale = 8 +local z = require("zprite").zchunk.new(tex, 384) +z._height_scale = 10 -for i = 1, 64 do - for j = 1, 64 do - local layers = love.math.simplexNoise(i * 0.02, j * 0.02) * 38 + 2 - z:put(i * 24, j * 24, BIG_BLOCK, math.floor(layers) + 1, layers < 23 and dirt_color or mountain_color) +local function update_map() + for i = -32, 32 do + for j = -32, 32 do + local layers = love.math.simplexNoise(i * 0.02, j * 0.02) * 28 + 4 + z:put(i * 24, j * 24, BIG_BLOCK, math.floor(layers) + 1, layers < 23 and dirt_color or mountain_color) + coroutine.yield() + end end end +local co = coroutine.create(update_map) + local angle = 0 function love.update(dt) - angle = math.fmod(angle + dt * 1.2, 2 * math.pi) + if coroutine.status(co) == "suspended" then + coroutine.resume(co) + end + angle = math.fmod(angle + dt * 0.8, 2 * math.pi) end function love.draw() - z:draw( - love.graphics.getWidth() / 2, - love.graphics.getHeight() / 2 + 64, - angle, - 0.35, - 0.35, - 24 * 64 / 2, - 24 * 64 / 2 - ) + z:draw(love.graphics.getWidth() / 2, love.graphics.getHeight() / 2 + 128, angle, 0.3, 0.3) end diff --git a/zprite/init.lua b/zprite/init.lua index 084ea23..dea1f3e 100644 --- a/zprite/init.lua +++ b/zprite/init.lua @@ -1,166 +1,9 @@ ----@class zprite ----@field private _texture love.Image ----@field private _instance_vertex_count integer ----@field private _mesh love.Mesh ----@field private _instance_buffer love.Buffer ----@field private _instances {integer: {}[]} ----@field private _top_layer integer ----@field private _height_scale number ----@field private _changed boolean ----@field private _count integer -local zprite = {} -zprite.__index = zprite +---@type zprite +local zprite = require("zprite.zprite") +---@type zchunk +local zchunk = require("zprite.zchunk") -local ZPRITE_SHADER = love.graphics.newShader([[ -varying vec4 VsColorOut; - -#ifdef VERTEX -layout (location = 3) attribute vec2 InstancePosition; -layout (location = 4) attribute vec2 InstanceTextureOffset; -layout (location = 5) attribute vec2 InstanceScale; -layout (location = 6) attribute float InstanceLayer; -layout (location = 7) attribute vec4 InstanceColor; - -uniform float HeightScale; - -vec4 position(mat4 transform_projection, vec4 vertex_position) -{ - vertex_position.xy *= InstanceScale; - vertex_position.xy += InstancePosition.xy; - vertex_position.z = InstanceLayer / 1024; - VaryingTexCoord.xy *= InstanceScale; - VaryingTexCoord.xy += InstanceTextureOffset; - VsColorOut = InstanceColor; - return (transform_projection * vertex_position) + vec4(0, (InstanceLayer * HeightScale) / love_ScreenSize.y, 0, 0); +return { + zprite = zprite, + zchunk = zchunk, } -#endif - -#ifdef PIXEL -vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) -{ - vec4 texture_color = Texel(tex, texture_coords); - return texture_color * color * VsColorOut; -} -#endif -]]) - -local function ZPRITE_DEFAULT_COLORMAP(_) - return 1, 1, 1, 1 -end - ----@param texture love.Image ----@param max_instances integer? -function zprite.new(texture, max_instances) - if not texture then - error("texture cannot be nil") - end - if max_instances and max_instances < 1 then - error("max_instances cannot be less than 1") - end - - local tw, th = texture:getPixelDimensions() - local vertices = { - { 0, 0, 0, 0 }, - { 1, 0, 1 / tw, 0 }, - { 0, 1, 0, 1 / th }, - { 1, 1, 1 / tw, 1 / th }, - } - - local obj = setmetatable({}, zprite) - - obj._texture = texture - obj._instance_vertex_count = max_instances or 16384 - obj._mesh = love.graphics.newMesh({ - { name = "VertexPosition", format = "floatvec2", location = 0 }, - { name = "VertexTexCoord", format = "floatvec2", location = 1 }, - }, vertices, "strip", "static") - obj._mesh:setTexture(texture) - obj._instance_buffer = love.graphics.newBuffer({ - { name = "InstancePosition", format = "floatvec2", location = 3 }, - { name = "InstanceTextureOffset", format = "floatvec2", location = 4 }, - { name = "InstanceScale", format = "floatvec2", location = 5 }, - { name = "InstanceLayer", format = "float", location = 6 }, - { name = "InstanceColor", format = "floatvec4", location = 7 }, - }, obj._instance_vertex_count, { vertex = true }) - obj._instances = {} - obj._height_scale = 4 - obj._changed = false - - obj._mesh:attachAttribute(3, obj._instance_buffer, "perinstance") - obj._mesh:attachAttribute(4, obj._instance_buffer, "perinstance") - obj._mesh:attachAttribute(5, obj._instance_buffer, "perinstance") - obj._mesh:attachAttribute(6, obj._instance_buffer, "perinstance") - obj._mesh:attachAttribute(7, obj._instance_buffer, "perinstance") - - obj._top_layer = 0 - obj._count = 0 - - return obj -end - ---- Clear all drawn objects -function zprite:clear() - -- no need to set _changed to true, as #self._instances will be 0 - self._instances = {} - self._top_layer = 0 - self._count = 0 -end - ---- Add an 'entity' to be rendered ----@param x number ----@param y number ----@param quad love.Quad ----@param layer_count integer ----@param color_map function? ----@param base_layer integer? -function zprite:put(x, y, quad, layer_count, color_map, base_layer) - if #self._instances + layer_count > self._instance_vertex_count then - return - end - - base_layer = base_layer or 0 - color_map = color_map or ZPRITE_DEFAULT_COLORMAP - - self._changed = true - - local qx, qy, qw, qh = quad:getViewport() - local tw, th = self._texture:getPixelDimensions() - - for i = 1, layer_count do - local r, g, b, a = color_map(i) - if not a then - a = 1 - end - if not self._instances[i + base_layer] then - self._instances[i + base_layer] = {} - if i > self._top_layer then - self._top_layer = i - end - end - table.insert(self._instances[i], { x, y, qx / tw, qy / th, qw, qh, i + base_layer, r, g, b, a }) - self._count = self._count + 1 - end -end - ---- Draw to the screen / current canvas ----@overload fun(self, x: number, y: number, r: number?, sx: number?, sy: number?, ox: number?, oy: number?, kx: number?, ky: number?) ----@overload fun(self, transform: love.Transform) -function zprite:draw(...) - love.graphics.setShader(ZPRITE_SHADER) - ZPRITE_SHADER:send("HeightScale", self._height_scale) - - if self._changed then - local current = 1 - for i = 1, self._top_layer do - if self._instances[i] then - self._instance_buffer:setArrayData(self._instances[i], 1, current, #self._instances[i]) - current = current + #self._instances[i] - end - end - self._changed = false - end - - love.graphics.drawInstanced(self._mesh, self._count, ...) -end - -return zprite diff --git a/zprite/zchunk.lua b/zprite/zchunk.lua new file mode 100644 index 0000000..0b4c179 --- /dev/null +++ b/zprite/zchunk.lua @@ -0,0 +1,112 @@ +local zprite = require("zprite.zprite") + +---@class zchunk +---@field private _chunks {integer: {z: zprite, x: integer, y: integer}} +---@field private _chunk_size integer +---@field private _texture love.Image +---@field private _height_scale number +local zchunk = {} +zchunk.__index = zchunk + +---@param texture love.Image +---@param chunk_size integer? +function zchunk.new(texture, chunk_size) + chunk_size = chunk_size or 16 + if chunk_size < 1 then + error("chunk size must be at least 1") + end + if not texture then + error("must have a texture") + end + + local obj = setmetatable({}, zchunk) + + obj._chunks = {} + obj._chunk_size = chunk_size + + obj._texture = texture + + obj._height_scale = 4 + + return obj +end + +---@param x integer +---@param y integer +---@return integer +function zchunk:_chunk_index(x, y) + return math.floor(x / self._chunk_size) + math.floor(y / self._chunk_size) * 1e7 +end + +---@param x integer +---@param y integer +function zchunk:remove(x, y) + local index = self:_chunk_index(x, y) + if not self._chunks[index] then + return + end + self._chunks[index] = {} +end + +---@param x integer +---@param y integer +function zchunk:clear(x, y) + local index = self:_chunk_index(x, y) + if not self._chunks[index] then + return + end + self._chunks[index].z:clear() +end + +--- Add an 'entity' to be rendered +---@param x number +---@param y number +---@param quad love.Quad +---@param layer_count integer +---@param color_map fun(integer): number, number, number, number? +---@param base_layer integer? +function zchunk:put(x, y, quad, layer_count, color_map, base_layer) + local index = self:_chunk_index(x, y) + if not self._chunks[index] then + self._chunks[index] = { + z = zprite.new(self._texture, self._chunk_size * self._chunk_size * 32), + x = math.floor(x / self._chunk_size), + y = math.floor(y / self._chunk_size), + } + self._chunks[index].z._height_scale = self._height_scale + end + self._chunks[index].z:put(x, y, quad, layer_count, color_map, base_layer) +end + +local transform = love.math.newTransform() +local function sort_fn(a, b) + local _, ay_rotated = transform:transformPoint(a.x, a.y) + local _, by_rotated = transform:transformPoint(b.x, b.y) + + return ay_rotated < by_rotated +end + +--- Draw to the screen / current canvas +---@overload fun(self, x: number, y: number, r: number?, sx: number?, sy: number?, ox: number?, oy: number?, kx: number?, ky: number?) +---@overload fun(self, transform: love.Transform) +function zchunk:draw(x, y, r, ...) + local chunks = {} + for _, chunk in pairs(self._chunks) do + table.insert(chunks, chunk) + end + + transform:reset() + if type(x) ~= "number" then + transform:apply(x) + else + transform:rotate(r and r or 0) + end + + table.sort(chunks, sort_fn) + + for _, chunk in ipairs(chunks) do + chunk.z:draw(x, y, r, ...) + end +end + +return zchunk diff --git a/zprite/zprite.lua b/zprite/zprite.lua new file mode 100644 index 0000000..802b748 --- /dev/null +++ b/zprite/zprite.lua @@ -0,0 +1,166 @@ +---@class zprite +---@field private _texture love.Image +---@field private _instance_vertex_count integer +---@field private _mesh love.Mesh +---@field private _instance_buffer love.Buffer +---@field private _instances {integer: {}[]} +---@field private _top_layer integer +---@field private _height_scale number +---@field private _changed boolean +---@field private _count integer +local zprite = {} +zprite.__index = zprite + +local ZPRITE_SHADER = love.graphics.newShader([[ +varying vec4 VsColorOut; + +#ifdef VERTEX +layout (location = 3) attribute vec2 InstancePosition; +layout (location = 4) attribute vec2 InstanceTextureOffset; +layout (location = 5) attribute vec2 InstanceScale; +layout (location = 6) attribute float InstanceLayer; +layout (location = 7) attribute vec4 InstanceColor; + +uniform float HeightScale; + +vec4 position(mat4 transform_projection, vec4 vertex_position) +{ + vertex_position.xy *= InstanceScale; + vertex_position.xy += InstancePosition.xy; + vertex_position.z = InstanceLayer / 1024; + VaryingTexCoord.xy *= InstanceScale; + VaryingTexCoord.xy += InstanceTextureOffset; + VsColorOut = InstanceColor; + return (transform_projection * vertex_position) + vec4(0, (InstanceLayer * HeightScale) / love_ScreenSize.y, 0, 0); +} +#endif + +#ifdef PIXEL +vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) +{ + vec4 texture_color = Texel(tex, texture_coords); + return texture_color * color * VsColorOut; +} +#endif +]]) + +local function ZPRITE_DEFAULT_COLORMAP(_) + return 1, 1, 1, 1 +end + +---@param texture love.Image +---@param max_instances integer? +function zprite.new(texture, max_instances) + if not texture then + error("must have a texture") + end + if max_instances and max_instances < 1 then + error("max_instances must be at least 1") + end + + local tw, th = texture:getPixelDimensions() + local vertices = { + { 0, 0, 0, 0 }, + { 1, 0, 1 / tw, 0 }, + { 0, 1, 0, 1 / th }, + { 1, 1, 1 / tw, 1 / th }, + } + + local obj = setmetatable({}, zprite) + + obj._texture = texture + obj._instance_vertex_count = max_instances or 16384 + obj._mesh = love.graphics.newMesh({ + { name = "VertexPosition", format = "floatvec2", location = 0 }, + { name = "VertexTexCoord", format = "floatvec2", location = 1 }, + }, vertices, "strip", "static") + obj._mesh:setTexture(texture) + obj._instance_buffer = love.graphics.newBuffer({ + { name = "InstancePosition", format = "floatvec2", location = 3 }, + { name = "InstanceTextureOffset", format = "floatvec2", location = 4 }, + { name = "InstanceScale", format = "floatvec2", location = 5 }, + { name = "InstanceLayer", format = "float", location = 6 }, + { name = "InstanceColor", format = "floatvec4", location = 7 }, + }, obj._instance_vertex_count, { vertex = true }) + obj._instances = {} + obj._height_scale = 4 + obj._changed = false + + obj._mesh:attachAttribute(3, obj._instance_buffer, "perinstance") + obj._mesh:attachAttribute(4, obj._instance_buffer, "perinstance") + obj._mesh:attachAttribute(5, obj._instance_buffer, "perinstance") + obj._mesh:attachAttribute(6, obj._instance_buffer, "perinstance") + obj._mesh:attachAttribute(7, obj._instance_buffer, "perinstance") + + obj._top_layer = 0 + obj._count = 0 + + return obj +end + +--- Clear all drawn objects +function zprite:clear() + -- no need to set _changed to true, as #self._instances will be 0 + self._instances = {} + self._top_layer = 0 + self._count = 0 +end + +--- Add an 'entity' to be rendered +---@param x number +---@param y number +---@param quad love.Quad +---@param layer_count integer +---@param color_map function? +---@param base_layer integer? +function zprite:put(x, y, quad, layer_count, color_map, base_layer) + if #self._instances + layer_count > self._instance_vertex_count then + return + end + + base_layer = base_layer or 0 + color_map = color_map or ZPRITE_DEFAULT_COLORMAP + + self._changed = true + + local qx, qy, qw, qh = quad:getViewport() + local tw, th = self._texture:getPixelDimensions() + + for i = 1, layer_count do + local r, g, b, a = color_map(i) + if not a then + a = 1 + end + if not self._instances[i + base_layer] then + self._instances[i + base_layer] = {} + if i > self._top_layer then + self._top_layer = i + end + end + table.insert(self._instances[i], { x, y, qx / tw, qy / th, qw, qh, i + base_layer, r, g, b, a }) + self._count = self._count + 1 + end +end + +--- Draw to the screen / current canvas +---@overload fun(self, x: number, y: number, r: number?, sx: number?, sy: number?, ox: number?, oy: number?, kx: number?, ky: number?) +---@overload fun(self, transform: love.Transform) +function zprite:draw(...) + love.graphics.setShader(ZPRITE_SHADER) + ZPRITE_SHADER:send("HeightScale", self._height_scale) + + if self._changed then + local current = 1 + for i = 1, self._top_layer do + if self._instances[i] then + self._instance_buffer:setArrayData(self._instances[i], 1, current, #self._instances[i]) + current = current + #self._instances[i] + end + end + self._changed = false + end + + love.graphics.drawInstanced(self._mesh, self._count, ...) +end + +return zprite