mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 14:51:03 +02:00
326c595336
Every figure is now its current 2D frame on one flat quad, the shader's alpha discard cutting the silhouette. It still faces south and leans by the camera's pitch, so it reads face-on at every tilt as before. Removes the contoured slab and the carved visual-hull models -- lib/VoxelModels.lua, tools/build_voxels.py and ~70 generated files under assets/voxels/ (2 MB). A sprite is a drawing, not an object seen from one side: Gen 1's overworld figures are 16x16 icons with a fixed front-on reading, and solidifying one invents a body the artist never drew. Those files were also the one place this mod carried a description of the ROM art, since a carve records a sprite's silhouette pixel for pixel. The card needs no pixel access, and the solid draw, the sun pass and the player's occlusion silhouette now share one mesh -- which the inverted depth test the silhouette uses requires, since self-overlap would repaint the figure on open ground. overrides/voxels/<name>.lua still wins where a mod ships one.
85 lines
3.4 KiB
Lua
85 lines
3.4 KiB
Lua
-- Voxel world mode: characters as flat forward-facing sprite billboards.
|
|
--
|
|
-- Every character -- the player, NPCs, the ghosts standing on a neighbour
|
|
-- map -- is its CURRENT 2D sprite frame on a single flat quad. The sheets
|
|
-- carry real alpha and the shader discards it, so the quad cuts the
|
|
-- sprite's exact silhouette out of itself; no geometry is built from the
|
|
-- pixels and nothing about a sprite is voxelized.
|
|
--
|
|
-- That is deliberate. A sprite is a DRAWING, not an object seen from one
|
|
-- side: Gen 1's overworld figures are 16x16 icons with a fixed front-on
|
|
-- reading, and turning one into a solid -- whether a contoured slab or a
|
|
-- carved visual hull -- reconstructs a body the artist never drew and the
|
|
-- game never implied. It also had the mod ship a description of the ROM
|
|
-- art. One quad wearing the real frame is both more faithful and cheaper:
|
|
-- it needs no pixel access at all, only the sheet's dimensions.
|
|
--
|
|
-- The card always faces SOUTH -- the direction the 2D game implies -- and
|
|
-- only LEANS BACK, pivoting at its feet, by exactly the camera's pitch
|
|
-- (VoxelScene's billboardMatrix), so at every tilt level it reads face-on
|
|
-- like the flat game. Right-facing and the alternating walk step are
|
|
-- matrix mirrors, not extra meshes. UVs point into the live sheet image,
|
|
-- so RED++ OBP bakes, SGB palette bakes and sprite-replacing mods all
|
|
-- texture it with no rebuild.
|
|
|
|
-- the mod namespace (see main.lua): V.require loads a sibling module
|
|
local V = ...
|
|
|
|
local Assets = require("src.render.Assets")
|
|
local Voxel3D = V.require("Voxel3D")
|
|
|
|
local SpriteBillboards = {}
|
|
|
|
local meshes = {}
|
|
|
|
-- One flat 16x16 quad UV-mapped to a whole frame. A hair of inset keeps
|
|
-- the sampler inside this frame rather than picking up the neighbouring
|
|
-- one along the shared edge.
|
|
local function buildCard(def, frame)
|
|
local ok, img = pcall(Assets.image, def.image)
|
|
if not (ok and img) then return nil end
|
|
local iw, ih = img:getDimensions()
|
|
local fy = frame * 16
|
|
if fy + 16 > ih then fy = 0 end
|
|
local u0, u1 = 0.02 / iw, (16 - 0.02) / iw
|
|
local v0, v1 = (fy + 0.05) / ih, (fy + 15.95) / ih
|
|
local verts = {
|
|
{ 0, 0, 0, u0, v1, 1 }, { 16, 0, 0, u1, v1, 1 },
|
|
{ 16, 16, 0, u1, v0, 1 }, { 0, 16, 0, u0, v0, 1 },
|
|
}
|
|
local indices = {}
|
|
Voxel3D.pushQuad(indices, 0)
|
|
return Voxel3D.newMesh(verts, indices)
|
|
end
|
|
|
|
-- The card for one (sprite def, frame index), or nil (headless / no
|
|
-- image), cached like every other derived GPU object.
|
|
--
|
|
-- The solid draw, the sun pass and the player's occlusion silhouette all
|
|
-- take THIS mesh. That the three agree is load-bearing, not tidiness: the
|
|
-- silhouette is drawn with the depth test INVERTED, so any self-overlap in
|
|
-- the mesh would read as "behind something" and repaint the figure on open
|
|
-- ground whether or not anything hides it; and the sun must see the same
|
|
-- outline the camera does, or a shadow stops matching what casts it.
|
|
function SpriteBillboards.mesh(def, frame)
|
|
local key = def.image .. "#" .. frame
|
|
if meshes[key] == nil then
|
|
local ok, m = pcall(buildCard, def, frame)
|
|
meshes[key] = (ok and m) or false
|
|
end
|
|
return meshes[key] or nil
|
|
end
|
|
|
|
-- Kept as its own name because the shadow and ghost passes read as their
|
|
-- own thing at the call sites; it once carried a different mesh from the
|
|
-- solid draw, and now deliberately does not.
|
|
SpriteBillboards.shadowQuad = SpriteBillboards.mesh
|
|
|
|
function SpriteBillboards.invalidate()
|
|
meshes = {}
|
|
end
|
|
|
|
Assets.register(SpriteBillboards.invalidate)
|
|
|
|
return SpriteBillboards
|