mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 15:50:52 +02:00
Merge branch 'worktree-viridian-forest' into 20260726_various_fixes
# Conflicts: # data/voxel_heights.lua
This commit is contained in:
+179
-53
@@ -519,15 +519,17 @@ end
|
||||
local ROUND_SHADE = { front = 1.0, back = 0.68, side = 0.78,
|
||||
top = 1.0, bottom = 0.55 }
|
||||
|
||||
local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
|
||||
N = N or 16 -- art canvas: 16 = one cell, 32 = 2x2 cells
|
||||
local N2 = N / 2
|
||||
local perRow = map.tileset.tilesPerRow or 16
|
||||
local atlasW = map.tileset.imageWidth or 128
|
||||
local atlasH = map.tileset.imageHeight or 48
|
||||
|
||||
-- cell-space art access (16x16, row 0 = top)
|
||||
-- cell-space art access (NxN, row 0 = top), anchored at cell (cx, cy)
|
||||
local function tileOf(px, py)
|
||||
return S.tileAt[keyOf(cx * 2 + (px >= 8 and 1 or 0),
|
||||
cy * 2 + (py >= 8 and 1 or 0))]
|
||||
return S.tileAt[keyOf(cx * 2 + math.floor(px / 8),
|
||||
cy * 2 + math.floor(py / 8))]
|
||||
end
|
||||
local function texel(px, py)
|
||||
local tile = tileOf(px, py)
|
||||
@@ -535,18 +537,18 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
math.floor(tile / perRow) * 8 + py % 8
|
||||
end
|
||||
|
||||
-- shade class of every cell pixel, indexed py * 16 + px
|
||||
-- shade class of every canvas pixel, indexed py * N + px
|
||||
local cls = {}
|
||||
for py = 0, 15 do
|
||||
for px = 0, 15 do
|
||||
for py = 0, N - 1 do
|
||||
for px = 0, N - 1 do
|
||||
local ax, ay = texel(px, py)
|
||||
local r, g, b, a = data:getPixel(ax, ay)
|
||||
cls[py * 16 + px] = a == 0 and "off"
|
||||
or Structures.shadeClass(math.min(r, g, b))
|
||||
cls[py * N + px] = a == 0 and "off"
|
||||
or Structures.shadeClass(math.min(r, g, b))
|
||||
end
|
||||
end
|
||||
|
||||
-- 4-connected flood from the cell border through `passable` classes
|
||||
-- 4-connected flood from the canvas border through `passable` classes
|
||||
local function floodOutside(passable)
|
||||
local out, stack = {}, {}
|
||||
local function seed(i)
|
||||
@@ -555,16 +557,16 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
stack[#stack + 1] = i
|
||||
end
|
||||
end
|
||||
for i = 0, 15 do
|
||||
seed(i); seed(240 + i); seed(i * 16); seed(i * 16 + 15)
|
||||
for i = 0, N - 1 do
|
||||
seed(i); seed(N * (N - 1) + i); seed(i * N); seed(i * N + N - 1)
|
||||
end
|
||||
while #stack > 0 do
|
||||
local i = table.remove(stack)
|
||||
local px = i % 16
|
||||
local px = i % N
|
||||
if px > 0 then seed(i - 1) end
|
||||
if px < 15 then seed(i + 1) end
|
||||
if i >= 16 then seed(i - 16) end
|
||||
if i < 240 then seed(i + 16) end
|
||||
if px < N - 1 then seed(i + 1) end
|
||||
if i >= N then seed(i - N) end
|
||||
if i < N * (N - 1) then seed(i + N) end
|
||||
end
|
||||
return out
|
||||
end
|
||||
@@ -573,23 +575,48 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
local out = floodOutside({ off = true, dark = true,
|
||||
light = true, white = true })
|
||||
local mask, enclosed = {}, 0
|
||||
for i = 0, 255 do
|
||||
for i = 0, N * N - 1 do
|
||||
if not out[i] then
|
||||
mask[i] = true
|
||||
if cls[i] ~= "black" then enclosed = enclosed + 1 end
|
||||
end
|
||||
end
|
||||
if enclosed < 32 then
|
||||
if enclosed < N * N / 8 then
|
||||
out = floodOutside({ off = true, light = true, white = true })
|
||||
mask = {}
|
||||
for i = 0, 255 do
|
||||
for i = 0, N * N - 1 do
|
||||
if not out[i] and cls[i] ~= "off" then mask[i] = true end
|
||||
end
|
||||
end
|
||||
local any = nil
|
||||
for i = 0, 255 do any = any or mask[i] end
|
||||
for i = 0, N * N - 1 do any = any or mask[i] end
|
||||
if not any then return {} end
|
||||
|
||||
-- a CAPPED hull (the stump): the top capRows rows of the mask are the
|
||||
-- drawn cut face -- a surface seen at an angle, not body. Strip them
|
||||
-- from the mask and remember their art span; the top-face quads below
|
||||
-- project that ellipse across the round cap.
|
||||
local capY0, capY1 = nil, nil
|
||||
if capRows and capRows > 0 then
|
||||
local top = nil
|
||||
for iy = 0, N - 1 do
|
||||
for ix = 0, N - 1 do
|
||||
if mask[iy * N + ix] then top = iy break end
|
||||
end
|
||||
if top then break end
|
||||
end
|
||||
if top then
|
||||
capY0 = top
|
||||
capY1 = math.min(top + capRows - 1, N - 2)
|
||||
for iy = capY0, capY1 do
|
||||
for ix = 0, N - 1 do mask[iy * N + ix] = nil end
|
||||
end
|
||||
any = nil
|
||||
for i = 0, N * N - 1 do any = any or mask[i] end
|
||||
if not any then return {} end
|
||||
end
|
||||
end
|
||||
|
||||
-- the ground the ball stands on: the drawing's own background names
|
||||
-- it. Score every flat ground tile the map places against the cell's
|
||||
-- unmasked light pixels and keep the closest -- mid-forest trees have
|
||||
@@ -604,9 +631,9 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
local ox = (t % perRow) * 8
|
||||
local oy = math.floor(t / perRow) * 8
|
||||
local score, n = 0, 0
|
||||
for py = 0, 15 do
|
||||
for px = 0, 15 do
|
||||
local i = py * 16 + px
|
||||
for py = 0, N - 1 do
|
||||
for px = 0, N - 1 do
|
||||
local i = py * N + px
|
||||
local c = cls[i]
|
||||
if not mask[i] and (c == "light" or c == "white") then
|
||||
local ax, ay = texel(px, py)
|
||||
@@ -629,10 +656,10 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
local z0, z1, src = {}, {}, {}
|
||||
local loRow, hiRow = {}, {}
|
||||
local yBot = nil
|
||||
for iy = 0, 15 do
|
||||
for iy = 0, N - 1 do
|
||||
local lo, hi = nil, nil
|
||||
for ix = 0, 15 do
|
||||
if mask[iy * 16 + ix] then
|
||||
for ix = 0, N - 1 do
|
||||
if mask[iy * N + ix] then
|
||||
lo = lo or ix
|
||||
hi = ix
|
||||
end
|
||||
@@ -643,7 +670,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
local c = (lo + hi + 1) / 2
|
||||
local hw = (hi - lo + 1) / 2
|
||||
for ix = lo, hi do
|
||||
local i = iy * 16 + ix
|
||||
local i = iy * N + ix
|
||||
if mask[i] then
|
||||
local dx = ix + 0.5 - c
|
||||
local n = 1
|
||||
@@ -651,7 +678,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
n = math.max(1, math.floor(2 * math.sqrt(hw * hw - dx * dx)
|
||||
+ 0.5))
|
||||
end
|
||||
z0[i] = math.floor(8 - n / 2 + 0.5)
|
||||
z0[i] = math.floor(N2 - n / 2 + 0.5)
|
||||
z1[i] = z0[i] + n
|
||||
src[i] = iy
|
||||
end
|
||||
@@ -661,28 +688,46 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
|
||||
-- foot: rows under the mask repeat the bottom row's discs, wearing the
|
||||
-- bottom row's (outline-dark) pixels
|
||||
for iy = yBot + 1, 15 do
|
||||
for iy = yBot + 1, N - 1 do
|
||||
loRow[iy], hiRow[iy] = loRow[yBot], hiRow[yBot]
|
||||
for ix = loRow[yBot], hiRow[yBot] do
|
||||
local b = yBot * 16 + ix
|
||||
local b = yBot * N + ix
|
||||
if z0[b] then
|
||||
local i = iy * 16 + ix
|
||||
local i = iy * N + ix
|
||||
z0[i], z1[i], src[i] = z0[b], z1[b], yBot
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- the round cap's top row and z extent, for the stump's ring
|
||||
-- projection below
|
||||
local capTopRow, capZ0, capZ1 = nil, nil, nil
|
||||
if capY0 then
|
||||
for iy = 0, N - 1 do
|
||||
if loRow[iy] then capTopRow = iy break end
|
||||
end
|
||||
if capTopRow then
|
||||
for ix = loRow[capTopRow], hiRow[capTopRow] do
|
||||
local i = capTopRow * N + ix
|
||||
if z0[i] then
|
||||
capZ0 = math.min(capZ0 or z0[i], z0[i])
|
||||
capZ1 = math.max(capZ1 or z1[i], z1[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function solidAt(ix, iy, iz)
|
||||
if ix < 0 or ix > 15 or iy < 0 or iy > 15 then return false end
|
||||
local i = iy * 16 + ix
|
||||
if ix < 0 or ix > N - 1 or iy < 0 or iy > N - 1 then return false end
|
||||
local i = iy * N + ix
|
||||
return z0[i] ~= nil and iz >= z0[i] and iz < z1[i]
|
||||
end
|
||||
|
||||
-- cap interiors sample the canopy a couple of rows below the rim,
|
||||
-- skipping outline-dark pixels
|
||||
local function deepTexel(ix, iy)
|
||||
for iy2 = iy + 2, math.min(15, iy + 4) do
|
||||
local i = iy2 * 16 + ix
|
||||
for iy2 = iy + 2, math.min(N - 1, iy + 4) do
|
||||
local i = iy2 * N + ix
|
||||
if mask[i] and cls[i] ~= "black" then return texel(ix, iy2) end
|
||||
end
|
||||
return texel(ix, iy)
|
||||
@@ -694,12 +739,12 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
-- ball paints solid black the moment the camera turns. The foot rows
|
||||
-- stay dark on purpose: their whole source row is outline-black.
|
||||
local function sideTexel(ix, iy)
|
||||
local r = src[iy * 16 + ix]
|
||||
local r = src[iy * N + ix]
|
||||
local dir = ix + ix < loRow[iy] + hiRow[iy] and 1 or -1
|
||||
for step = 0, 3 do
|
||||
local x2 = ix + dir * step
|
||||
local i2 = r * 16 + x2
|
||||
if x2 < 0 or x2 > 15 or not mask[i2] then break end
|
||||
local i2 = r * N + x2
|
||||
if x2 < 0 or x2 > N - 1 or not mask[i2] then break end
|
||||
if cls[i2] ~= "black" then return texel(x2, r) end
|
||||
end
|
||||
return texel(ix, r)
|
||||
@@ -707,20 +752,20 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
|
||||
local quads = {}
|
||||
|
||||
for iy = 0, 15 do
|
||||
for iy = 0, N - 1 do
|
||||
if loRow[iy] then
|
||||
local yB, yT = 15 - iy, 16 - iy
|
||||
local yB, yT = N - 1 - iy, N - iy
|
||||
|
||||
-- front and back: the drawing per-pixel, columns merged where they
|
||||
-- share a chord plane; a run never crosses the 8px atlas tile seam
|
||||
-- (its u range must interpolate inside one tile)
|
||||
local ix = loRow[iy]
|
||||
while ix <= hiRow[iy] do
|
||||
local i = iy * 16 + ix
|
||||
local i = iy * N + ix
|
||||
if z0[i] then
|
||||
local ix2 = ix
|
||||
while ix2 + 1 <= hiRow[iy] do
|
||||
local j = iy * 16 + ix2 + 1
|
||||
local j = iy * N + ix2 + 1
|
||||
if z0[j] == z0[i] and z1[j] == z1[i]
|
||||
and math.floor((ix2 + 1) / 8) == math.floor(ix / 8) then
|
||||
ix2 = ix2 + 1
|
||||
@@ -732,8 +777,8 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
local ax1 = (texel(ix2, src[i]))
|
||||
local u0, u1 = (ax0 + 0.05) / atlasW, (ax1 + 0.95) / atlasW
|
||||
local v0, v1 = (ay + 0.05) / atlasH, (ay + 0.95) / atlasH
|
||||
local x0, x1 = ix - 8, ix2 - 7
|
||||
local zF, zB = z1[i] - 8, z0[i] - 8
|
||||
local x0, x1 = ix - N2, ix2 - N2 + 1
|
||||
local zF, zB = z1[i] - N2, z0[i] - N2
|
||||
quads[#quads + 1] = {
|
||||
{ x0, yB, zF }, { x1, yB, zF }, { x1, yT, zF }, { x0, yT, zF },
|
||||
uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } },
|
||||
@@ -753,11 +798,11 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
-- sides, steps, undersides: constant-texel quads over the z runs a
|
||||
-- neighbour doesn't cover
|
||||
for ix = loRow[iy], hiRow[iy] do
|
||||
local i = iy * 16 + ix
|
||||
local i = iy * N + ix
|
||||
if z0[i] then
|
||||
local ax, ay = texel(ix, src[i])
|
||||
local u, v = (ax + 0.5) / atlasW, (ay + 0.5) / atlasH
|
||||
local x0, x1 = ix - 8, ix - 7
|
||||
local x0, x1 = ix - N2, ix - N2 + 1
|
||||
|
||||
-- exposed z pieces against one neighbouring column
|
||||
local function pieces(nx, ny, emit)
|
||||
@@ -768,7 +813,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
while iz2 + 1 < z1[i] and not solidAt(nx, ny, iz2 + 1) do
|
||||
iz2 = iz2 + 1
|
||||
end
|
||||
emit(iz - 8, iz2 - 7, iz, iz2)
|
||||
emit(iz - N2, iz2 - N2 + 1, iz, iz2)
|
||||
iz = iz2 + 1
|
||||
else
|
||||
iz = iz + 1
|
||||
@@ -797,7 +842,20 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
u = tu, v = tv, shade = ROUND_SHADE.top,
|
||||
}
|
||||
end
|
||||
if izA == z0[i] and izB == z1[i] - 1 and izB - izA >= 2 then
|
||||
if capTopRow and iy == capTopRow and capZ1 then
|
||||
-- the CUT FACE (a capped hull's top): project the drawn
|
||||
-- ellipse across the round cap voxel row by voxel row --
|
||||
-- its top arc at the cap's north rim, its bottom arc at
|
||||
-- the south, the perspective the 2D art already implies
|
||||
for iz = izA, izB do
|
||||
local t = capZ1 - 1 > capZ0
|
||||
and (iz - capZ0) / (capZ1 - 1 - capZ0) or 0
|
||||
local ry = capY0 + math.floor(t * (capY1 - capY0) + 0.5)
|
||||
local cax, cay = texel(ix, ry)
|
||||
top(iz - N2, iz - N2 + 1,
|
||||
(cax + 0.5) / atlasW, (cay + 0.5) / atlasH)
|
||||
end
|
||||
elseif izA == z0[i] and izB == z1[i] - 1 and izB - izA >= 2 then
|
||||
-- the dome cap: outline on the rim cells, canopy inside
|
||||
local du, dv = deepTexel(ix, iy)
|
||||
top(zA, zA + 1, u, v)
|
||||
@@ -807,7 +865,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
top(zA, zB, u, v)
|
||||
end
|
||||
end)
|
||||
if iy < 15 then
|
||||
if iy < N - 1 then
|
||||
pieces(ix, iy + 1, function(zA, zB)
|
||||
quads[#quads + 1] = {
|
||||
{ x0, yB, zB }, { x1, yB, zB }, { x1, yB, zA }, { x0, yB, zA },
|
||||
@@ -855,23 +913,91 @@ function Structures.buildCylinders(S, map, x0, x1, y0, y1, groundTiles)
|
||||
end
|
||||
local tsid = tostring(map.tileset.id or map.tileset.image or "?")
|
||||
|
||||
-- the stump class's drawn-ellipse height, hand-authored per tileset
|
||||
-- (the profile's stump_cap, in art rows)
|
||||
local stumpCap = 6
|
||||
do
|
||||
local okP, prof = pcall(V.data, "voxel_heights")
|
||||
local entry = okP and type(prof) == "table" and prof.tilesets
|
||||
and prof.tilesets[map.tileset.id]
|
||||
if entry and type(entry.stump_cap) == "number" then
|
||||
stumpCap = entry.stump_cap
|
||||
end
|
||||
end
|
||||
|
||||
-- cells consumed by a 2x2 `canopy` group; the scan runs north to
|
||||
-- south, west to east, so an anchor always claims its partners
|
||||
-- before they are visited
|
||||
local grouped = {}
|
||||
for cy = math.floor(y0 / 2), math.floor(y1 / 2) do
|
||||
for cx = math.floor(x0 / 2), math.floor(x1 / 2) do
|
||||
Budget.tick()
|
||||
local ckey = cy * 8192 + cx
|
||||
local k = keyOf(cx * 2, cy * 2)
|
||||
local s = S.shapeAt[k]
|
||||
local s = (not grouped[ckey]) and S.shapeAt[k] or nil
|
||||
local near = cx * 2 >= -ROUND_RING and cx * 2 < tw + ROUND_RING
|
||||
and cy * 2 >= -ROUND_RING and cy * 2 < th + ROUND_RING
|
||||
if s and s.art == "cylinder" and near then
|
||||
if s and s.art == "canopy" and near then
|
||||
-- ONE 32px hull over the 2x2-cell drawing. The partner cells
|
||||
-- must be round-pinned too, or the drawing is partial (a map
|
||||
-- edit, a mod's stray anchor tile) and the anchor is left
|
||||
-- alone rather than carved into a half-empty giant.
|
||||
local whole = true
|
||||
for _, d in ipairs({ { 1, 0 }, { 0, 1 }, { 1, 1 } }) do
|
||||
local ps = S.shapeAt[keyOf((cx + d[1]) * 2, (cy + d[2]) * 2)]
|
||||
if not (ps and (ps.art == "cylinder" or ps.art == "canopy")) then
|
||||
whole = false
|
||||
end
|
||||
end
|
||||
if whole then
|
||||
local ground = false
|
||||
if data then
|
||||
local ids = {}
|
||||
for dy = 0, 3 do
|
||||
for dx = 0, 3 do
|
||||
ids[#ids + 1] = S.tileAt[keyOf(cx * 2 + dx, cy * 2 + dy)]
|
||||
end
|
||||
end
|
||||
local sig = tsid .. "|g32|" .. gsig .. "|"
|
||||
.. table.concat(ids, ":")
|
||||
local tpl = roundCache[sig]
|
||||
if not tpl then
|
||||
local tq, tbg = roundTemplate(S, map, data, cx, cy,
|
||||
groundTiles, 32)
|
||||
tpl = { quads = tq, bg = tbg }
|
||||
roundCache[sig] = tpl
|
||||
end
|
||||
ground = tpl.bg or false
|
||||
S.roundStamps[#S.roundStamps + 1] =
|
||||
{ quads = tpl.quads, mx = cx * 16 + 16, mz = cy * 16 + 16,
|
||||
r = 16 }
|
||||
end
|
||||
for dy = 0, 3 do
|
||||
for dx = 0, 3 do
|
||||
local tk = keyOf(cx * 2 + dx, cy * 2 + dy)
|
||||
S.skip[tk] = true
|
||||
S.ground[tk] = ground
|
||||
end
|
||||
end
|
||||
grouped[ckey + 1] = true
|
||||
grouped[ckey + 8192] = true
|
||||
grouped[ckey + 8193] = true
|
||||
end
|
||||
elseif s and s.art == "cylinder" and near then
|
||||
-- a `stump`-class cell is the same hull with a cut face: its
|
||||
-- top capRows of drawing project onto the round top
|
||||
local cap = s.class == "stump" and stumpCap or nil
|
||||
local ground = false
|
||||
if data then
|
||||
local sig = tsid .. "|" .. gsig .. "|" .. table.concat({
|
||||
local sig = tsid .. (cap and ("|c" .. cap) or "") .. "|"
|
||||
.. gsig .. "|" .. table.concat({
|
||||
S.tileAt[k], S.tileAt[keyOf(cx * 2 + 1, cy * 2)],
|
||||
S.tileAt[keyOf(cx * 2, cy * 2 + 1)],
|
||||
S.tileAt[keyOf(cx * 2 + 1, cy * 2 + 1)] }, ":")
|
||||
local tpl = roundCache[sig]
|
||||
if not tpl then
|
||||
local tq, tbg = roundTemplate(S, map, data, cx, cy, groundTiles)
|
||||
local tq, tbg = roundTemplate(S, map, data, cx, cy,
|
||||
groundTiles, 16, cap)
|
||||
tpl = { quads = tq, bg = tbg }
|
||||
roundCache[sig] = tpl
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user