mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 15:10:51 +02:00
456 lines
20 KiB
Lua
456 lines
20 KiB
Lua
-- Voxel world mode: assemble and draw one frame of the 3D scene.
|
|
--
|
|
-- World space is world pixels and shares its origin with the 2D paths, so
|
|
-- the terrain mesh needs no transform at all and a connected map just
|
|
-- translates by the same (ox, oy) the flat renderer already offsets it by.
|
|
--
|
|
-- Order is: the sun's shadow pass, then terrain, then characters, then a 2D
|
|
-- overlay for the field FX. There is no y-sort anywhere -- the depth buffer
|
|
-- resolves occlusion, which is the whole point of the mode. Walk behind a
|
|
-- building and the building is simply in front.
|
|
|
|
-- the mod namespace (see main.lua): V.require loads a sibling module
|
|
local V = ...
|
|
|
|
local Mat4 = V.require("Mat4")
|
|
local Voxel3D = V.require("Voxel3D")
|
|
local ShadowMap = V.require("ShadowMap")
|
|
local ChunkMesher = V.require("ChunkMesher")
|
|
local VoxelModels = V.require("VoxelModels")
|
|
local SpriteBillboards = V.require("SpriteBillboards")
|
|
local TileShape = V.require("TileShape")
|
|
local TerrainAtlas = V.require("TerrainAtlas")
|
|
local PaletteFX = require("src.render.PaletteFX")
|
|
|
|
local VoxelScene = {}
|
|
|
|
-- What the active display mode actually paints with.
|
|
--
|
|
-- paletteFor hands back a map's RAW SGB zone palette, and that is not what
|
|
-- any of the non-colour modes draw. The flat path runs it through
|
|
-- PaletteFX.effectiveColors on the way to the shade-remap shader, and that
|
|
-- call IS where GRAY, INVERTED and CLASSIC happen -- OG / OG INV replace
|
|
-- the palette with the DMG greys (inverted for the latter), CLASSIC
|
|
-- replaces it with the green DMG set, and GBC INV permutes the zone's own
|
|
-- shades. GBC and RED++ pass through untouched.
|
|
--
|
|
-- This pass has no shader to apply that in: colour is baked into the atlas
|
|
-- and into the sprite sheets ahead of the draw, so it has to run the same
|
|
-- transform itself. Without it every mode that is not already a colour mode
|
|
-- comes through wearing the SGB palette -- grey and inverted both rendering
|
|
-- as plain SGB blue.
|
|
local function modeColors(paletteFor, map)
|
|
local c = paletteFor and paletteFor(map) or nil
|
|
return PaletteFX.effectiveColors(c)
|
|
end
|
|
|
|
VoxelScene._modeColors = modeColors -- named for the suite
|
|
|
|
-- A facing is a yaw about +Y. Models are carved facing +Z (south, "down"),
|
|
-- and Mat4.rotateY(+90 deg) maps +Z to +X, so east is +90 and west is -90.
|
|
-- Nothing is mirrored: yawing to face east shows the model's east side,
|
|
-- which carries the left-view art -- exactly the mirror the 2D game draws
|
|
-- for right-facing, arrived at by turning the model instead of flipping it.
|
|
local YAW = {
|
|
down = 0,
|
|
up = math.pi,
|
|
right = math.pi / 2,
|
|
left = -math.pi / 2,
|
|
}
|
|
|
|
-- The ground height a cell stands at, so a character on a ledge stands on
|
|
-- top of it rather than sunk into it. Uses the same bottom-left collision
|
|
-- tile the engine walks on (Map:cellTile).
|
|
local function groundAt(map, cellX, cellY)
|
|
-- Off the map, cellTile border-extends into the map's borderBlock --
|
|
-- which on maps ringed with trees is a RAISED tile. The only entity
|
|
-- ever standing off-map is the player mid seam-step (placed one cell
|
|
-- before the connection entry), and the ground actually rendered
|
|
-- there is the departed neighbour's flat walkway: height 0. Without
|
|
-- this, crossing into such a map hoisted the walker tree-high for
|
|
-- exactly one step -- the "hops like a ledge" seam bug.
|
|
if not map:inBounds(cellX, cellY) then return 0 end
|
|
local shapes = TileShape.forMap(map)
|
|
local s = shapes[map:cellTile(cellX, cellY)]
|
|
if not s then return 0 end
|
|
-- a recessed class (water) still supports whatever stands on it; only
|
|
-- raised ground lifts the model. Stairs never do: the class height is
|
|
-- the flight's TALL end, but the player enters at floor level and the
|
|
-- warp fires as they step in -- lifting them onto the geometry read as
|
|
-- climbing an invisible block
|
|
if s.art == "stair" then return 0 end
|
|
return s.h > 0 and s.h or 0
|
|
end
|
|
|
|
-- Model matrix for a 16x16 character whose cell's top-left is world
|
|
-- (px, py), standing on ground height `gh`, yawed to `facing`. The yaw is
|
|
-- taken about the cell centre, not the model corner, so turning in place
|
|
-- keeps the character on its own tile.
|
|
local function characterMatrix(px, py, gh, facing, mirror)
|
|
local m = Mat4.mul(Mat4.translate(px + 8, gh, py + 8),
|
|
Mat4.rotateY(YAW[facing] or 0))
|
|
if mirror then
|
|
m = Mat4.mul(m, Mat4.scale(-1, 1, 1))
|
|
end
|
|
return Mat4.mul(m, Mat4.translate(-8, 0, -8))
|
|
end
|
|
|
|
VoxelScene.characterMatrix = characterMatrix
|
|
VoxelScene.YAW = YAW
|
|
|
|
-- Character style: "billboard" renders each entity as its current sprite
|
|
-- frame voxelized with side-view relief, leaning back toward the camera;
|
|
-- "model" is the carved visual-hull figure that turns with its facing.
|
|
VoxelScene.style = "billboard"
|
|
|
|
-- Camera-ward pull distance for billboards (and the grass rows, which
|
|
-- must keep their relative depth to feet): just enough that a leaned-back
|
|
-- slab clears the wall it leans over. The lean flattens toward top-down,
|
|
-- so the needed pull grows exactly as real occlusion stops mattering.
|
|
function VoxelScene.pull(a)
|
|
return 6 + math.max(0, 16 * math.cos(a) - 8) / math.max(math.sin(a), 0.2)
|
|
end
|
|
|
|
-- The sheet frame and mirror flag the 2D path would draw for this pose
|
|
-- (same tables as SpriteRenderer). Shared by the billboard pass and the
|
|
-- shadow pass so a walking character's shadow swings its legs too.
|
|
local function frameFor(def, facing, phase, flip)
|
|
local SR = require("src.render.SpriteRenderer")
|
|
local frame, mirror = 0, false
|
|
if (def.frames or 1) > 1 then
|
|
frame = (def.walker and phase == 1) and SR.WALK[facing]
|
|
or SR.STAND[facing]
|
|
mirror = facing == "right"
|
|
or ((facing == "down" or facing == "up") and phase == 1 and flip)
|
|
end
|
|
return frame, mirror
|
|
end
|
|
|
|
-- FALLBACK ONLY (see castShadows below). Draw one entity's drop shadow as
|
|
-- a decal: its current sprite frame as a single quad, flattened onto the
|
|
-- ground along the sun line (Voxel3D.shadowMatrix). Runs inside
|
|
-- beginShadows, which supplies the translucent black; the texture is only
|
|
-- consulted for its alpha, so no palette work is needed.
|
|
local function drawShadow(sprite, px, py, facing, phase, flip, gh, lift)
|
|
local def = sprite.def
|
|
local frame, mirror = frameFor(def, facing, phase, flip)
|
|
local mesh = SpriteBillboards.shadowQuad(def, frame)
|
|
if not mesh then return end
|
|
Voxel3D.draw(mesh, sprite:resolveImage(),
|
|
Voxel3D.shadowMatrix(px, py, gh, lift, mirror))
|
|
end
|
|
|
|
-- Draw one posed entity. Returns true if 3D geometry carried it, false
|
|
-- when nothing could be built and the caller should fall back.
|
|
-- `colors` is the 4-color world palette the entity stands under in the SGB
|
|
-- modes (nil under RED++/trueColor): the 2D path colorizes sprites with a
|
|
-- screen-space shader the voxel canvas never runs through, so the model's
|
|
-- texture gets the palette baked in instead (TerrainAtlas.forSprite).
|
|
-- `lift` raises the figure off the ground plane (ledge hops arc UP in 3D,
|
|
-- where the 2D path could only slide the sprite north).
|
|
local function drawEntity(sprite, px, py, facing, phase, flip, gh, colors,
|
|
lift)
|
|
local def = sprite.def
|
|
local tex = sprite:resolveImage()
|
|
if colors and not def.trueColor then
|
|
tex = TerrainAtlas.forSprite(def.image, colors) or tex
|
|
end
|
|
local y = gh + (lift or 0)
|
|
|
|
if VoxelScene.style == "billboard" then
|
|
-- pick the very frame the 2D path would draw (same tables). The slab
|
|
-- always faces SOUTH -- the direction the 2D game implies -- and only
|
|
-- LEANS BACK, pivoting at its feet, by exactly the camera's pitch, so
|
|
-- at every tilt level the sprite reads face-on like the flat game.
|
|
-- No camera-tracking yaw: every sprite leans in parallel.
|
|
local frame, mirror = frameFor(def, facing, phase, flip)
|
|
local mesh = SpriteBillboards.mesh(def, frame)
|
|
if mesh then
|
|
local Voxel = V.require("VoxelState")
|
|
local pitch = Voxel.angle - math.pi / 2
|
|
-- Camera-ward pull (applied per vertex in the shader, along each
|
|
-- vertex's own eye ray, so it is a PURE depth bias with zero screen
|
|
-- drift): lets the leaned-back head win against the wall it leans
|
|
-- OVER while a character genuinely BEHIND a building is dozens of
|
|
-- pixels deeper and still loses, so real occlusion works.
|
|
local a = math.max(Voxel.angle, 0.05)
|
|
local pull = VoxelScene.pull(a)
|
|
local m = Mat4.mul(Mat4.translate(px + 8, y, py + 8),
|
|
Mat4.rotateX(pitch))
|
|
if mirror then m = Mat4.mul(m, Mat4.scale(-1, 1, 1)) end
|
|
-- the slab is built centered on its sprite plane (z = 0), so only
|
|
-- the x anchor shifts; the relief bulges symmetrically front/back
|
|
m = Mat4.mul(m, Mat4.translate(-8, 0, 0))
|
|
-- the same slab UNLEANED is what the sun saw (castShadows draws
|
|
-- exactly this card), so that is where each vertex asks whether the
|
|
-- light reached it -- see Voxel3D.draw
|
|
Voxel3D.draw(mesh, tex, m, pull, Voxel3D.casterMatrix(px, py, y, mirror))
|
|
return true
|
|
end
|
|
-- no pixel access: fall through to the carved model
|
|
end
|
|
|
|
local mesh = VoxelModels.mesh(def, phase == 1 and "walk" or "stand")
|
|
if not mesh then return false end
|
|
-- alternate walk steps mirror the down/up frames in 2D (the GB uses an
|
|
-- OAM flip); mirroring the model about its own centre is the same idea
|
|
local mirror = flip and phase == 1 and (facing == "down" or facing == "up")
|
|
Voxel3D.draw(mesh, tex, characterMatrix(px, py, y, facing, mirror))
|
|
return true
|
|
end
|
|
|
|
VoxelScene.drawEntity = drawEntity
|
|
|
|
-- Render the world. `state` is the OverworldState; `vw`/`vh` the world view
|
|
-- size in world pixels; `w`/`h` the pixel size of the canvas to render
|
|
-- into; `paletteFor(map)` yields a map's 4-color world palette (nil in the
|
|
-- color modes whose atlas is already true color). Returns the finished
|
|
-- canvas, or nil if the 3D pass could not run (headless, no depth support)
|
|
-- so the caller can fall back to 2D.
|
|
-- The last live-set key, so eviction only runs when the neighbourhood
|
|
-- actually changes (a map crossing), not every frame.
|
|
local lastLiveKey = nil
|
|
|
|
-- Request everything `state`'s frame wants and evict what it no longer
|
|
-- does; returns the current map's terrain mesh (or nil while it builds)
|
|
-- and the neighbour meshes ready to draw. render() calls this for the
|
|
-- frame it is drawing, and the pipeline's update hook calls it EVERY
|
|
-- frame -- including the frames a warp's Transition covers, when the
|
|
-- world pass is off. That update-side call is what lets a door fade hide
|
|
-- the destination's build: the map swaps behind the fade, and waiting
|
|
-- for the first visible frame to request meshes would show the flat
|
|
-- fallback while the first slices run.
|
|
function VoxelScene.prefetch(state)
|
|
local Voxel = V.require("VoxelState")
|
|
|
|
-- The live set is the current map plus its rendered neighbours. When
|
|
-- it changes, everything outside it (and the previous set, which
|
|
-- ChunkMesher retains so stepping into a house keeps the town warm)
|
|
-- is evicted -- meshes released, analysis dropped -- so memory stays
|
|
-- bounded by the neighbourhood instead of growing with every area
|
|
-- ever visited.
|
|
local liveKey = state.map.id
|
|
local live = { [state.map.id] = true }
|
|
for _, nb in ipairs(state.neighbors or {}) do
|
|
live[nb.map.id] = true
|
|
liveKey = liveKey .. "|" .. nb.map.id
|
|
end
|
|
if liveKey ~= lastLiveKey then
|
|
lastLiveKey = liveKey
|
|
ChunkMesher.setLive(live)
|
|
-- RED++ bakes one atlas per map, so its animated copy is per map too
|
|
-- and is bounded by the same neighbourhood
|
|
TerrainAtlas.setLive(live)
|
|
end
|
|
|
|
-- masks: where connected neighbour BODIES sit, so the border ring is
|
|
-- suppressed under them (see runGeometry)
|
|
local masks = {}
|
|
for _, nb in ipairs(state.neighbors or {}) do
|
|
masks[#masks + 1] = { nb.ox, nb.oy,
|
|
nb.ox + nb.map.def.width * 32,
|
|
nb.oy + nb.map.def.height * 32 }
|
|
end
|
|
|
|
-- Builds are asynchronous (ChunkMesher.pump runs in the pipeline's
|
|
-- update): request what this frame wants and draw what is ready.
|
|
-- The current map draws its body-only mesh while the full one (the
|
|
-- border ring) is still building -- a seam crossing promotes a
|
|
-- neighbour whose body is already cached, and the ring pops in a few
|
|
-- frames later, mostly hidden behind the map just left. A neighbour
|
|
-- missing its body-only mesh draws its cached FULL mesh instead -- a
|
|
-- crossing demotes the map just left, and it must not vanish from
|
|
-- behind the player while its body variant builds; its ring is
|
|
-- already masked out under this map's body, so the stand-in is safe.
|
|
local terrain = ChunkMesher.request(state.map, false, masks, true)
|
|
if not terrain then
|
|
terrain = ChunkMesher.peek(state.map, true)
|
|
end
|
|
local nbMesh = {}
|
|
for i, nb in ipairs(state.neighbors or {}) do
|
|
nbMesh[i] = ChunkMesher.request(nb.map, true)
|
|
or ChunkMesher.peek(nb.map, false)
|
|
end
|
|
Voxel.ready = terrain ~= nil
|
|
return terrain, nbMesh
|
|
end
|
|
|
|
-- Capture every entity's pose for this frame. pose() advances the hop /
|
|
-- surf bob / spinner timers, so it must be called EXACTLY once per entity
|
|
-- per frame -- the sun pass and the character pass then read the same
|
|
-- answer instead of disagreeing by a tick. Ghost NPCs live on a neighbour
|
|
-- map, so their position, ground lookup and palette all belong to that
|
|
-- map. pose() returns the VISUAL y (ledge hops arc it, surfing bobs it);
|
|
-- the difference from the entity's base y becomes vertical LIFT in 3D, so
|
|
-- a hop rises off the ground instead of sliding north.
|
|
local function posesOf(state, spriteColors)
|
|
local colors = spriteColors(state.map)
|
|
local posed = {}
|
|
for _, g in ipairs(state.ghosts or {}) do
|
|
local sprite, vx, vy, facing, phase, flip = g.npc:pose()
|
|
posed[#posed + 1] = {
|
|
sprite = sprite, px = vx + g.ox, py = g.npc.py + g.oy,
|
|
facing = facing, phase = phase, flip = flip,
|
|
gh = groundAt(g.map or state.map, g.npc.cellX, g.npc.cellY),
|
|
lift = g.npc.py - vy, colors = spriteColors(g.map or state.map),
|
|
}
|
|
end
|
|
for _, e in ipairs(state.entities or {}) do
|
|
if not (state.flyAnim and e == state.player) then
|
|
local sprite, vx, vy, facing, phase, flip = e:pose()
|
|
posed[#posed + 1] = {
|
|
sprite = sprite, px = vx, py = e.py,
|
|
facing = facing, phase = phase, flip = flip,
|
|
gh = groundAt(state.map, e.cellX, e.cellY),
|
|
lift = e.py - vy, colors = colors,
|
|
}
|
|
end
|
|
end
|
|
return posed
|
|
end
|
|
|
|
-- A stamp of everything the sun pass depends on. Nothing in it moving
|
|
-- means the shadow map it produced last frame is still exactly right, and
|
|
-- redrawing the whole world from the sun would buy nothing -- which is
|
|
-- most of a dialog, a menu, or any moment standing still.
|
|
local sigBuf = {}
|
|
local function shadowSignature(terrain, nbMesh, posed, cx, cy, vw, vh)
|
|
local n = 0
|
|
local function put(v)
|
|
n = n + 1
|
|
sigBuf[n] = v
|
|
end
|
|
-- quarter-pixel camera granularity: the light frustum is snapped to
|
|
-- whole texels anyway, each a third of a world pixel
|
|
put(math.floor(cx * 4))
|
|
put(math.floor(cy * 4))
|
|
-- the view size and the camera PITCH are both what the light frustum is
|
|
-- fitted to (a lower camera sees further north, so the box grows), so a
|
|
-- zoom step, a window resize or a rung change invalidates the map even
|
|
-- standing perfectly still
|
|
put(vw); put(vh)
|
|
put(math.floor((V.require("VoxelState").angle or 0) * 512))
|
|
put(tostring(terrain))
|
|
for i = 1, #nbMesh do put(tostring(nbMesh[i])) end
|
|
for _, p in ipairs(posed) do
|
|
put(p.sprite.def.image)
|
|
put(p.px); put(p.py); put(p.gh); put(p.lift or 0)
|
|
put(p.facing); put(p.phase); put(p.flip and 1 or 0)
|
|
end
|
|
for i = n + 1, #sigBuf do sigBuf[i] = nil end
|
|
return table.concat(sigBuf, ",")
|
|
end
|
|
|
|
-- The sun pass: render the scene once from the light, so the main pass can
|
|
-- ask any fragment whether the sun reached it. Every caster the main pass
|
|
-- draws goes in -- the terrain mesh, which is where buildings, trees,
|
|
-- ledges, signs and every prop live, plus one UPRIGHT card per character
|
|
-- (Voxel3D.casterMatrix; the leaning slab is a trick for the camera, not
|
|
-- for the sun) -- so shadows land on walls, roofs, ledges and passing NPCs
|
|
-- as readily as on the floor.
|
|
--
|
|
-- Runs BEFORE Voxel3D.beginScene, because canvases do not nest. Grass is
|
|
-- left out on purpose: thousands of tufts would cast a speckle no bigger
|
|
-- than the pixels it lands on, at the cost of the mesh being drawn twice.
|
|
local function castShadows(state, terrain, nbMesh, posed, cx, cy, vw, vh,
|
|
atlasFor)
|
|
if not ShadowMap.available() then return end
|
|
local sig = shadowSignature(terrain, nbMesh, posed, cx, cy, vw, vh)
|
|
if not ShadowMap.stale(sig) then return end
|
|
if not ShadowMap.begin(cx, cy, vw, vh) then return end
|
|
|
|
ShadowMap.draw(terrain, atlasFor(state.map), nil)
|
|
for i, nb in ipairs(state.neighbors or {}) do
|
|
ShadowMap.draw(nbMesh[i], atlasFor(nb.map),
|
|
Mat4.translate(nb.ox, 0, nb.oy))
|
|
end
|
|
for _, p in ipairs(posed) do
|
|
local def = p.sprite.def
|
|
local frame, mirror = frameFor(def, p.facing, p.phase, p.flip)
|
|
local mesh = SpriteBillboards.shadowQuad(def, frame)
|
|
if mesh then
|
|
ShadowMap.draw(mesh, p.sprite:resolveImage(),
|
|
Voxel3D.casterMatrix(p.px, p.py, p.gh + (p.lift or 0),
|
|
mirror))
|
|
end
|
|
end
|
|
|
|
ShadowMap.finish(sig)
|
|
end
|
|
|
|
function VoxelScene.render(state, w, h, vw, vh, paletteFor)
|
|
-- With nothing cached at all (the first frame of a fresh toggle),
|
|
-- return nil: the engine keeps the 2D path for the frame and
|
|
-- Voxel.ready holds the camera tween at flat, so the switch waits
|
|
-- invisibly instead of freezing or tilting an empty stage.
|
|
local terrain, nbMesh = VoxelScene.prefetch(state)
|
|
if not terrain then return nil end
|
|
|
|
local cam = state.camera
|
|
local cx, cy = cam.x + vw / 2, cam.y + vh / 2
|
|
|
|
local function atlasFor(map)
|
|
return TerrainAtlas.forMap(map, modeColors(paletteFor, map))
|
|
end
|
|
|
|
-- sprite palettes only exist in the SGB modes; under RED++ the OBP bake
|
|
-- inside sprite:resolveImage() already colors the sheet
|
|
local function spriteColors(map)
|
|
if PaletteFX.usesGbcPack() then return nil end
|
|
return modeColors(paletteFor, map)
|
|
end
|
|
|
|
local posed = posesOf(state, spriteColors)
|
|
castShadows(state, terrain, nbMesh, posed, cx, cy, vw, vh, atlasFor)
|
|
|
|
if not Voxel3D.beginScene(w, h, cx, cy, vw, vh) then return nil end
|
|
|
|
Voxel3D.draw(terrain, atlasFor(state.map), nil)
|
|
for i, nb in ipairs(state.neighbors or {}) do
|
|
Voxel3D.draw(nbMesh[i], atlasFor(nb.map),
|
|
Mat4.translate(nb.ox, 0, nb.oy))
|
|
end
|
|
|
|
-- Without a shadow map (headless, or a driver that could not make the
|
|
-- canvas) the old flat decals stand in: ground-only, characters only,
|
|
-- but better than a world with nothing under anybody. They go down
|
|
-- first, as decals the characters then stand over -- depth-tested
|
|
-- against the terrain just drawn (a shadow behind a building stays
|
|
-- hidden) but never depth-writing, so the grass pass at the end of the
|
|
-- frame still wins its feet-overdraw fights.
|
|
if not Voxel3D.shadowsActive() then
|
|
Voxel3D.beginShadows()
|
|
for _, p in ipairs(posed) do
|
|
drawShadow(p.sprite, p.px, p.py, p.facing, p.phase, p.flip, p.gh,
|
|
p.lift)
|
|
end
|
|
Voxel3D.endShadows()
|
|
end
|
|
|
|
-- characters, normally depth-tested: the camera-ward pull inside
|
|
-- drawEntity resolves the lean-over-the-wall-in-front case, and a
|
|
-- character genuinely behind a building is far deeper and loses the
|
|
-- test, so buildings and trees really occlude.
|
|
for _, p in ipairs(posed) do
|
|
drawEntity(p.sprite, p.px, p.py, p.facing, p.phase, p.flip, p.gh,
|
|
p.colors, p.lift)
|
|
end
|
|
-- tall grass last, pulled camera-ward exactly as far as the characters
|
|
-- were (same per-vertex shader bias, so grass never drifts either):
|
|
-- relative depth between a walker and the tuft row south of their feet
|
|
-- is preserved, so the row still overdraws feet -- the 3D version of
|
|
-- the GB's grass-over-feet trick -- while grass keeps losing to the
|
|
-- buildings it genuinely stands behind (far deeper than the pull).
|
|
local Voxel = V.require("VoxelState")
|
|
local pull = VoxelScene.pull(math.max(Voxel.angle, 0.05))
|
|
Voxel3D.draw(ChunkMesher.grass(state.map), atlasFor(state.map), nil, pull)
|
|
for _, nb in ipairs(state.neighbors or {}) do
|
|
Voxel3D.draw(ChunkMesher.grass(nb.map), atlasFor(nb.map),
|
|
Mat4.translate(nb.ox, 0, nb.oy), pull)
|
|
end
|
|
|
|
return Voxel3D.endScene()
|
|
end
|
|
|
|
return VoxelScene
|