mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 07:10:51 +02:00
Visual improvements: grass movement
This commit is contained in:
+8
-3
@@ -812,18 +812,23 @@ function ChunkMesher.build(map, bodyOnly, masks, split)
|
||||
return sink.finish(), waterSink and waterSink.finish() or nil
|
||||
end
|
||||
|
||||
local function quadsMesh(quads)
|
||||
local function quadsMesh(quads, grass)
|
||||
if #quads == 0 then return nil end
|
||||
local verts, indices, n = {}, {}, 0
|
||||
for _, q in ipairs(quads) do
|
||||
for i = 1, 4 do
|
||||
local c = q[i]
|
||||
local uv = q.uv and q.uv[i] or { q.u, q.v }
|
||||
verts[#verts + 1] = { c[1], c[2], c[3], uv[1], uv[2], q.shade }
|
||||
local v = { c[1], c[2], c[3], uv[1], uv[2], q.shade }
|
||||
if grass then
|
||||
v[7], v[8], v[9] = q.sway or 0, q.cx or 0, q.cz or 0
|
||||
end
|
||||
verts[#verts + 1] = v
|
||||
end
|
||||
Voxel3D.pushQuad(indices, n)
|
||||
n = n + 1
|
||||
end
|
||||
if grass then return Voxel3D.newGrassMesh(verts, indices) end
|
||||
return Voxel3D.newMesh(verts, indices)
|
||||
end
|
||||
|
||||
@@ -832,7 +837,7 @@ end
|
||||
-- walker's feet (characters stamp over terrain, Gen 1 style, so ordinary
|
||||
-- terrain could never do this).
|
||||
local function buildGrassMesh(map)
|
||||
return quadsMesh(Structures.forMap(map).grassQuads)
|
||||
return quadsMesh(Structures.forMap(map).grassQuads, true)
|
||||
end
|
||||
|
||||
-- The flower billboards as their own mesh, for the same reason as the
|
||||
|
||||
+5
-1
@@ -3508,13 +3508,17 @@ function Structures.buildGrass(S, map, x0, x1, y0, y1, data)
|
||||
templates[tileId] = tpl
|
||||
end
|
||||
local wx, wz = tx * 8, ty * 8
|
||||
-- Stable diagonal phase per tuft. Both ends of every quad receive
|
||||
-- the same value, so a gust bends the slab without shearing it.
|
||||
local sway = wx * 0.050 + wz * 0.031
|
||||
for _, q in ipairs(tpl) do
|
||||
quads[#quads + 1] = {
|
||||
{ q[1][1] + wx, q[1][2], q[1][3] + wz },
|
||||
{ q[2][1] + wx, q[2][2], q[2][3] + wz },
|
||||
{ q[3][1] + wx, q[3][2], q[3][3] + wz },
|
||||
{ q[4][1] + wx, q[4][2], q[4][3] + wz },
|
||||
uv = q.uv, shade = q.shade,
|
||||
uv = q.uv, shade = q.shade, sway = sway,
|
||||
cx = wx + 4, cz = wz + 4,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,6 +48,21 @@ Voxel3D.FORMAT = {
|
||||
{ "VertexShade", "float", 1 },
|
||||
}
|
||||
|
||||
-- Tall grass carries one extra value: a stable phase shared by every
|
||||
-- vertex in a tuft. Keeping it constant prevents the two ends of a blade
|
||||
-- from shearing apart while the gust travels across the map.
|
||||
Voxel3D.GRASS_FORMAT = {
|
||||
{ "VertexPosition", "float", 3 },
|
||||
{ "VertexTexCoord", "float", 2 },
|
||||
{ "VertexShade", "float", 1 },
|
||||
{ "VertexGrass", "float", 3 }, -- phase, clump centre x, clump centre z
|
||||
}
|
||||
|
||||
Voxel3D.GRASS_WIND_PIXELS = 1.15
|
||||
Voxel3D.GRASS_WIND_SPEED = 2.35
|
||||
Voxel3D.GRASS_INTERACT_RADIUS = 12
|
||||
Voxel3D.GRASS_INTERACT_PIXELS = 2.5
|
||||
|
||||
-- Face shading by direction id: top faces stay
|
||||
-- full brightness, sides step down so an extruded block reads as solid
|
||||
-- instead of a flat sticker, and the faces turned away from the sun are
|
||||
@@ -96,7 +111,11 @@ local SHADER = [[
|
||||
uniform float pull;
|
||||
uniform vec3 curve; // xy = the focus in world XZ, z = k; 0 = off
|
||||
uniform vec4 fogInfo; // density, start, heightK; density 0 = clear
|
||||
uniform vec4 grassWind; // enabled, time, wind pixels, speed
|
||||
uniform vec4 grassPlayer; // world x, world z, radius, push pixels
|
||||
uniform vec2 grassPrevious; // previous player world xz for swept contact
|
||||
attribute float VertexShade;
|
||||
attribute vec3 VertexGrass;
|
||||
vec4 position(mat4 transform_projection, vec4 vertex_position) {
|
||||
vShade = VertexShade;
|
||||
#ifdef VOXEL_GRID
|
||||
@@ -117,6 +136,32 @@ local SHADER = [[
|
||||
// answered. (The pull below is excluded for the same reason: it is a
|
||||
// depth trick aimed at the camera's own buffer.)
|
||||
vSun = (sunVP * (sunModel * vertex_position)).xyz;
|
||||
|
||||
// Wind and player contact are applied in world space, before the curved
|
||||
// world and camera pull. vertex y is 0..8 for these tuft meshes, which
|
||||
// pins the root and lets the tip receive the full displacement.
|
||||
if (grassWind.x > 0.5) {
|
||||
float bend = clamp(vertex_position.y / 8.0, 0.0, 1.0);
|
||||
bend *= bend;
|
||||
float wave = sin(grassWind.y * grassWind.w + VertexGrass.x);
|
||||
vec2 offset = vec2(wave * grassWind.z, 0.0);
|
||||
|
||||
// Move each touched tuft rigidly a little to the left or right. The
|
||||
// baked clump centre is identical for all its vertices, preventing
|
||||
// stretching, twisting and the ugly crushed-grass silhouette.
|
||||
vec2 clump = VertexGrass.yz;
|
||||
float bodyDist = length(clump - grassPlayer.xy);
|
||||
float touch = 1.0 - smoothstep(grassPlayer.z * 0.45,
|
||||
grassPlayer.z, bodyDist);
|
||||
float side = clump.x < grassPlayer.x ? -1.0 : 1.0;
|
||||
if (abs(clump.x - grassPlayer.x) < 0.5)
|
||||
side = sin(VertexGrass.x) < 0.0 ? -1.0 : 1.0;
|
||||
|
||||
// Preserve the existing wind bend. Player contact is only a small,
|
||||
// uniform X translation, just enough to suggest the grass parting.
|
||||
w.xz += offset * bend;
|
||||
w.x += side * touch * grassPlayer.w;
|
||||
}
|
||||
// THE MAP'S HAZE (see ForestAtmos): how much fog stands between the
|
||||
// eye and this vertex -- distance dissolves into it, altitude climbs
|
||||
// out of it. Worked out on the FLAT world like the shadow lookup
|
||||
@@ -496,6 +541,15 @@ function Voxel3D.newMesh(verts, map)
|
||||
return mesh
|
||||
end
|
||||
|
||||
function Voxel3D.newGrassMesh(verts, map)
|
||||
if #verts == 0 then return nil end
|
||||
local ok, mesh = pcall(love.graphics.newMesh, Voxel3D.GRASS_FORMAT, verts,
|
||||
"triangles", "static")
|
||||
if not ok then return nil end
|
||||
if map and #map > 0 then pcall(mesh.setVertexMap, mesh, map) end
|
||||
return mesh
|
||||
end
|
||||
|
||||
-- The quad corner offsets and UV corners for one face direction, in the
|
||||
-- order the vertex map below stitches into two triangles. Corners are unit
|
||||
-- offsets from the voxel's (x, y, z) minimum corner.
|
||||
@@ -1089,6 +1143,10 @@ function Voxel3D.beginScene(w, h, cx, cy, vw, vh, sky, slot)
|
||||
pcall(sh.send, sh, "glassGlint", Voxel3D.glassGlint or 0)
|
||||
-- on until a sprite pass says otherwise, reset per frame like `ghost`
|
||||
pcall(sh.send, sh, "glassOn", 1)
|
||||
-- still until the dedicated grass pass enables it
|
||||
pcall(sh.send, sh, "grassWind", { 0, 0, 0, 0 })
|
||||
pcall(sh.send, sh, "grassPlayer", { -100000, -100000, 1, 0 })
|
||||
pcall(sh.send, sh, "grassPrevious", { -100000, -100000 })
|
||||
-- the curved world bends about the camera's focus, so the horizon keeps
|
||||
-- a fixed distance ahead of the player rather than sitting on the map.
|
||||
-- A placed camera may decline it outright (Voxel3D.camera.curve = 0).
|
||||
@@ -1368,6 +1426,24 @@ function Voxel3D.glass(on)
|
||||
pcall(activeShader.send, activeShader, "glassOn", on and 1 or 0)
|
||||
end
|
||||
|
||||
-- Enable wind only around tall-grass draws. px/pz are the current player's
|
||||
-- feet in world pixels; previous values make collision continuous per frame.
|
||||
function Voxel3D.grassWind(on, px, pz, previousX, previousZ)
|
||||
if not (active and activeShader) then return end
|
||||
if not on then
|
||||
pcall(activeShader.send, activeShader, "grassWind", { 0, 0, 0, 0 })
|
||||
return
|
||||
end
|
||||
local now = love.timer and love.timer.getTime and love.timer.getTime() or 0
|
||||
pcall(activeShader.send, activeShader, "grassWind",
|
||||
{ 1, now, Voxel3D.GRASS_WIND_PIXELS, Voxel3D.GRASS_WIND_SPEED })
|
||||
pcall(activeShader.send, activeShader, "grassPlayer",
|
||||
{ px or -100000, pz or -100000,
|
||||
Voxel3D.GRASS_INTERACT_RADIUS, Voxel3D.GRASS_INTERACT_PIXELS })
|
||||
pcall(activeShader.send, activeShader, "grassPrevious",
|
||||
{ previousX or px or -100000, previousZ or pz or -100000 })
|
||||
end
|
||||
|
||||
function Voxel3D.endGhost()
|
||||
if not active then return end
|
||||
pcall(love.graphics.setDepthMode, "lequal", true)
|
||||
|
||||
@@ -1141,11 +1141,28 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
|
||||
-- so the tuft rows keep exactly the characters' own depth handicap
|
||||
local lean = math.max(leanAngle(), 0.05)
|
||||
local pull = VoxelScene.pull(lean)
|
||||
-- Character px/py is anchored on the 16 px card. Its world centre/feet
|
||||
-- contact used by the camera code is +8,+8, so use the same point here.
|
||||
-- Keep the prior rendered point so fast steps sweep through every tuft.
|
||||
local gx, gz = -100000, -100000
|
||||
if me then gx, gz = me.px + 8, me.py + 8 end
|
||||
VoxelScene._grassPrevX = VoxelScene._grassPrevX or gx
|
||||
VoxelScene._grassPrevZ = VoxelScene._grassPrevZ or gz
|
||||
-- A warp/map transition is not a walk. Do not sweep one enormous contact
|
||||
-- segment across the new map when the player jumps more than two tiles.
|
||||
local gdx, gdz = gx - VoxelScene._grassPrevX, gz - VoxelScene._grassPrevZ
|
||||
if gdx * gdx + gdz * gdz > 32 * 32 then
|
||||
VoxelScene._grassPrevX, VoxelScene._grassPrevZ = gx, gz
|
||||
end
|
||||
Voxel3D.grassWind(true, gx, gz,
|
||||
VoxelScene._grassPrevX, VoxelScene._grassPrevZ)
|
||||
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
|
||||
Voxel3D.grassWind(false)
|
||||
VoxelScene._grassPrevX, VoxelScene._grassPrevZ = gx, gz
|
||||
-- flower billboards: pulled like the characters and the grass, MINUS
|
||||
-- the depth of 8 world pixels along the view (8 sin a -- the camera
|
||||
-- looks along (0, -cos a, -sin a), so that is exactly one tile row of
|
||||
|
||||
Reference in New Issue
Block a user