mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 05:10:58 +02:00
fix water for android glsl shaders
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
name: Release
|
||||
|
||||
# Packs the mod into an installable .zip and publishes it as a GitHub Release,
|
||||
# once per push to main.
|
||||
#
|
||||
# Archive layout: every mod file at the archive root, manifest.json included.
|
||||
# That is one of the two shapes the game accepts on MODS > Import mod .zip
|
||||
# (src/mods/LauncherMods.lua locateRoot: manifest at the root, or inside a
|
||||
# single top-level folder). Nothing else is added, so the archive stays
|
||||
# installable by hand too.
|
||||
#
|
||||
# Versioning, first rule that applies wins:
|
||||
# 1. the "version" input of a manual run,
|
||||
# 2. "[release X.Y.Z]" anywhere in the commit message,
|
||||
# 3. manifest.json's own version, when it is ahead of every existing tag,
|
||||
# so bumping the manifest is the normal way to cut a release,
|
||||
# 4. otherwise the newest vX.Y.Z tag with its patch incremented
|
||||
# (0.2.99 rolls over to 0.3.0).
|
||||
# Whichever wins is written into the manifest.json inside the archive, so a
|
||||
# shipped mod never reports a different version than the release it came from.
|
||||
#
|
||||
# Generated by: python3 tools/modkit.py add-release-workflow <mod-id>
|
||||
# MOD_ID below is stamped to this mod's id when the file is copied.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Exact version to release (e.g. 0.3.0). Leave blank to auto-resolve."
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine version
|
||||
id: ver
|
||||
env:
|
||||
DISPATCH_VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY' >> "$GITHUB_OUTPUT"
|
||||
import json, os, re, subprocess, sys
|
||||
|
||||
SEMVER = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
||||
|
||||
def sh(*args):
|
||||
return subprocess.run(args, capture_output=True, text=True).stdout.strip()
|
||||
|
||||
def parse(text):
|
||||
m = SEMVER.match(text)
|
||||
return tuple(int(p) for p in m.groups()) if m else None
|
||||
|
||||
def die(msg):
|
||||
print(f"::error::{msg}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
with open("manifest.json", encoding="utf-8") as fh:
|
||||
manifest_version = str(json.load(fh).get("version", ""))
|
||||
|
||||
released = sorted(
|
||||
v for v in (parse(tag[1:]) for tag in sh("git", "tag", "-l", "v*").splitlines()) if v
|
||||
)
|
||||
latest = released[-1] if released else None
|
||||
|
||||
override = os.environ.get("DISPATCH_VERSION", "").strip()
|
||||
if not override:
|
||||
found = re.search(r"\[release\s+(\d+\.\d+\.\d+)\]", sh("git", "log", "-1", "--pretty=%B"))
|
||||
override = found.group(1) if found else ""
|
||||
|
||||
manifest_ver = parse(manifest_version)
|
||||
if override:
|
||||
version = parse(override) or die(f"invalid version override {override!r} (expected X.Y.Z)")
|
||||
source = "the override"
|
||||
elif manifest_ver and (latest is None or manifest_ver > latest):
|
||||
version = manifest_ver
|
||||
source = "manifest.json"
|
||||
elif latest:
|
||||
major, minor, patch = latest
|
||||
patch += 1
|
||||
if patch > 99:
|
||||
minor, patch = minor + 1, 0
|
||||
version = (major, minor, patch)
|
||||
source = "a patch bump on v%d.%d.%d" % latest
|
||||
else:
|
||||
die(f"manifest.json version {manifest_version!r} is not X.Y.Z "
|
||||
"and there is no vX.Y.Z tag to count from")
|
||||
|
||||
text = "%d.%d.%d" % version
|
||||
print(f"Releasing {text}, from {source}.", file=sys.stderr)
|
||||
print(f"version={text}")
|
||||
print(f"tag=v{text}")
|
||||
PY
|
||||
|
||||
- name: Refuse to clobber an existing release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ steps.ver.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
||||
echo "::error::Tag $TAG already exists. Pick a different version."
|
||||
exit 1
|
||||
fi
|
||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||
echo "::error::Release $TAG already exists. Pick a different version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build the mod .zip
|
||||
env:
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
MOD_ID: "DRAMATIC_SHAPE"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
staging="$RUNNER_TEMP/pkg"
|
||||
out="$GITHUB_WORKSPACE/dist"
|
||||
rm -rf "$staging" "$out"
|
||||
mkdir -p "$staging" "$out"
|
||||
|
||||
git archive HEAD | tar -x -C "$staging"
|
||||
|
||||
rm -rf "$staging/.github" "$staging/.gitattributes" \
|
||||
"$staging/.gitignore" "$staging/.luarc.json"
|
||||
|
||||
python3 - "$staging/manifest.json" "$VERSION" <<'PY'
|
||||
import json, sys
|
||||
path, version = sys.argv[1], sys.argv[2]
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
manifest = json.load(fh)
|
||||
manifest["version"] = version
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump(manifest, fh, indent=2, ensure_ascii=False)
|
||||
fh.write("\n")
|
||||
PY
|
||||
|
||||
zip_path="$out/${MOD_ID}-${VERSION}.zip"
|
||||
(cd "$staging" && zip -qr "$zip_path" .)
|
||||
unzip -l "$zip_path"
|
||||
|
||||
unzip -p "$zip_path" manifest.json > "$RUNNER_TEMP/packed-manifest.json"
|
||||
python3 - "$RUNNER_TEMP/packed-manifest.json" "$VERSION" <<'PY'
|
||||
import json, sys
|
||||
path, expected = sys.argv[1], sys.argv[2]
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
version = json.load(fh)["version"]
|
||||
if version != expected:
|
||||
raise SystemExit(f"::error::packed manifest says {version}, expected {expected}")
|
||||
print(f"manifest.json is at the archive root and reports {version}")
|
||||
PY
|
||||
|
||||
(cd "$out" && sha256sum "${MOD_ID}"-*.zip > sha256sums.txt)
|
||||
cat "$out/sha256sums.txt"
|
||||
|
||||
- name: Publish GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
TAG: ${{ steps.ver.outputs.tag }}
|
||||
MOD_ID: "DRAMATIC_SHAPE"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
prev="$(git tag -l 'v*' --sort=-v:refname | grep -v "^${TAG}$" | head -1 || true)"
|
||||
range="${prev:+${prev}..}$GITHUB_SHA"
|
||||
changes="$(git log --no-merges --pretty='- %s' "$range" | head -50 || true)"
|
||||
|
||||
notes=$'Download the .zip and install it from the game: MODS > Import mod .zip.'
|
||||
if [ -n "$changes" ]; then
|
||||
notes+=$'\n\n## Changes\n\n'"$changes"
|
||||
fi
|
||||
printf 'Release notes:\n%s\n' "$notes"
|
||||
|
||||
gh release create "$TAG" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "$VERSION" \
|
||||
--notes "$notes" \
|
||||
"dist/${MOD_ID}-${VERSION}.zip" \
|
||||
"dist/sha256sums.txt"
|
||||
|
||||
echo "Published release $TAG"
|
||||
@@ -147,6 +147,33 @@
|
||||
painted. The reflection lands a hair off the sprite's own depth and exactly
|
||||
on its colour, which at a lake's worth of wave is the same picture.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **On Android the water stayed flat, as if the row were off.** Two GLSL ES
|
||||
defaults the desktop never exercises, both in the water shader:
|
||||
|
||||
Fragment floats default to **mediump** on GLSL ES, and the vertex stage's
|
||||
default is highp -- and the water shader is the mod's first to declare the
|
||||
same uniform, the frame's `vp` matrix, in BOTH stages, one on each default.
|
||||
GLSL ES refuses to link a uniform whose precision the stages disagree on,
|
||||
so the whole shader failed to build and the pass fell back -- quietly, by
|
||||
design -- to the flat water the mode always drew. The pixel stage now lifts
|
||||
its float default to highp (guarded, so a GPU without fragment highp still
|
||||
compiles and falls back flat), which settles the link and is also simply
|
||||
needed: the march works in world coordinates that run to a few thousand,
|
||||
where fp16 has no fraction left, and the world-position varying is
|
||||
qualified highp for the same reason the wireframe's has been all along.
|
||||
Samplers default to **lowp** no matter what floats are set to, so the depth
|
||||
read is lifted too -- eight bits of depth is a march with nothing to land
|
||||
on.
|
||||
|
||||
And the readable depth canvas -- the one hardware requirement the rest of
|
||||
the mode does not already have -- now tries four formats before giving up:
|
||||
depth24, depth24 riding a stencil (a pairing some mobile drivers will
|
||||
texture when they refuse the bare format), depth32f, and depth16 as the
|
||||
floor every GLES3 device can read. Refused all four, the reflections are
|
||||
lost and nothing else, exactly as before.
|
||||
|
||||
### Known
|
||||
|
||||
- Screen-space reflections can only reflect what is in the frame. A tree just
|
||||
|
||||
+16
-7
@@ -295,15 +295,24 @@ local active = false
|
||||
-- never be sampled. An explicit canvas is the same buffer with a texture
|
||||
-- handle on it, and costs the same memory.
|
||||
--
|
||||
-- nil where the driver will not make one -- the format is optional in GLES
|
||||
-- and a canvas is the only honest test of it -- and beginScene falls
|
||||
-- straight back to the internal buffer, which is exactly the old behaviour
|
||||
-- minus the reflections.
|
||||
-- nil where the driver will not make one -- every depth format is optional
|
||||
-- in GLES and a canvas is the only honest test of any of them, so this asks
|
||||
-- for several in order of preference: 24 bits, the same 24 riding a stencil
|
||||
-- (a pairing some mobile drivers will texture when the bare format they
|
||||
-- refuse), 32-bit float, and 16 as the floor every GLES3 device can read.
|
||||
-- Refused all four, beginScene falls straight back to the internal buffer,
|
||||
-- which is exactly the old behaviour minus the reflections.
|
||||
local DEPTH_FORMATS = { "depth24", "depth24stencil8", "depth32f", "depth16" }
|
||||
|
||||
local function newDepth(w, h)
|
||||
if not (love.graphics and love.graphics.newCanvas) then return nil end
|
||||
local ok, c = pcall(love.graphics.newCanvas, w, h,
|
||||
{ format = "depth24", readable = true })
|
||||
if not (ok and c) then return nil end
|
||||
local c = nil
|
||||
for _, format in ipairs(DEPTH_FORMATS) do
|
||||
local ok, made = pcall(love.graphics.newCanvas, w, h,
|
||||
{ format = format, readable = true })
|
||||
if ok and made then c = made break end
|
||||
end
|
||||
if not c then return nil end
|
||||
-- nearest: a depth is a distance, and a blend of two of them is a
|
||||
-- distance to nothing. The march wants the texel it landed on.
|
||||
pcall(c.setFilter, c, "nearest", "nearest")
|
||||
|
||||
+26
-3
@@ -345,7 +345,12 @@ Water.EDGE_FADE = 0.14 -- reflection eased off over this much of the fra
|
||||
local SHADER_SRC = [[
|
||||
varying float vShade;
|
||||
varying vec3 vSun;
|
||||
varying vec3 vBent; // world position, as drawn
|
||||
// World position, as drawn -- and a varying that cannot ride GLSL ES's
|
||||
// mediump fragment default: everything below floors it into columns and
|
||||
// marches it through the frame's matrices, and a route's coordinates run
|
||||
// to a few thousand, where fp16 has no fraction left at all. The same
|
||||
// reasoning the scene shader's vGrid states at length.
|
||||
varying LOVE_HIGHP_OR_MEDIUMP vec3 vBent;
|
||||
|
||||
#ifdef VERTEX
|
||||
uniform mat4 vp;
|
||||
@@ -368,6 +373,21 @@ vec4 position(mat4 transform_projection, vec4 vertex_position) {
|
||||
#endif
|
||||
|
||||
#ifdef PIXEL
|
||||
// Everything below works in WORLD units through the frame's own matrices,
|
||||
// and GLSL ES defaults fragment floats to mediump -- fp16, out of fraction
|
||||
// by a coordinate of two thousand and quantising a depth into steps the
|
||||
// march falls straight through. Worse than wrong pictures: `vp` is
|
||||
// declared by BOTH stages, the vertex side's default is highp, and GLSL ES
|
||||
// refuses to LINK a uniform whose precision the two stages disagree on --
|
||||
// which is not broken water but NO water shader at all, the flat fallback
|
||||
// with nothing in the log. One statement lifts the whole stage; the guard
|
||||
// keeps the odd GPU without fragment highp compiling, and such a driver
|
||||
// falls back to flat water exactly as it did before this pass existed.
|
||||
#ifdef GL_ES
|
||||
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
||||
precision highp float;
|
||||
#endif
|
||||
#endif
|
||||
uniform mat4 vp;
|
||||
uniform vec3 eye;
|
||||
uniform vec2 screen; // the canvas, in pixels
|
||||
@@ -381,9 +401,12 @@ uniform float sunBias;
|
||||
uniform vec2 sunTexel;
|
||||
uniform vec3 dayTint;
|
||||
|
||||
// the frame as it stood before the water went down, and its depth
|
||||
// the frame as it stood before the water went down, and its depth. The
|
||||
// depth sampler is qualified because GLSL ES defaults samplers to LOWP no
|
||||
// matter what floats are set to, and eight bits of depth is a march with
|
||||
// nothing to land on. The frame copy is honest 8-bit colour and can stay.
|
||||
uniform Image reflectTex;
|
||||
uniform Image depthTex;
|
||||
uniform LOVE_HIGHP_OR_MEDIUMP Image depthTex;
|
||||
|
||||
uniform float rays; // 0 = sky only, 1 = march the screen too
|
||||
uniform vec3 lookFlat; // the way the horizon lies from this camera
|
||||
|
||||
@@ -1778,6 +1778,25 @@ T.check(gridded:find("#define VOXEL_GRID", 1, true) ~= nil,
|
||||
T.check(plain:find("//@CRATERS", 1, true) == nil,
|
||||
"and the crater placeholder is gone by the time a driver sees the source")
|
||||
|
||||
-- ANDROID. GLSL ES defaults fragment floats to mediump and samplers to
|
||||
-- lowp, and this shader is the one place in the mod where both defaults
|
||||
-- are fatal: world coordinates run past fp16's fraction, the depth read
|
||||
-- rounds to steps the march falls straight through, and -- the sharp edge
|
||||
-- -- `vp` is declared by BOTH stages, whose defaults disagree, which GLSL
|
||||
-- ES answers by refusing to LINK the shader at all. Flat lakes, empty log.
|
||||
-- The sky's band ramp is this same lesson learned once already.
|
||||
T.check(plain:find("precision highp float;", 1, true) ~= nil,
|
||||
"the pixel stage lifts GLSL ES's mediump default to highp, so the march "
|
||||
.. "keeps its fraction and the dual-declared vp links at one precision")
|
||||
T.check(plain:find("GL_FRAGMENT_PRECISION_HIGH", 1, true) ~= nil,
|
||||
"guarded, so the odd GPU without fragment highp still compiles and "
|
||||
.. "falls back flat instead of failing loudly")
|
||||
T.check(plain:find("LOVE_HIGHP_OR_MEDIUMP vec3 vBent", 1, true) ~= nil,
|
||||
"the world-position varying is qualified like the scene shader's vGrid "
|
||||
.. "rather than left to the fragment default")
|
||||
T.check(plain:find("LOVE_HIGHP_OR_MEDIUMP Image depthTex", 1, true) ~= nil,
|
||||
"and the depth sampler is lifted off lowp, which is eight bits of depth")
|
||||
|
||||
-- ------- the lift itself
|
||||
--
|
||||
-- A pond in a field: four water cells recessed below flat ground. The
|
||||
|
||||
@@ -74,7 +74,8 @@ return function(game)
|
||||
-- hardware requirement the rest of the mode does not already have.
|
||||
say("depth canvas readable: %s", tostring(Voxel3D.depthReadable()))
|
||||
if not Voxel3D.depthReadable() then
|
||||
say("STOP: no readable depth canvas on this driver (depth24/readable).")
|
||||
say("STOP: no readable depth canvas on this driver (tried depth24, ")
|
||||
say(" depth24stencil8, depth32f and depth16).")
|
||||
say(" The water falls back to the flat scene shader, which is")
|
||||
say(" exactly what it looked like before this feature existed.")
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user