Lighting fixes on transparent textures.
This commit is contained in:
parent
caac38336c
commit
b4376c0cfc
11 changed files with 269 additions and 36 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -22,3 +22,4 @@ docs/.asciidoctor/
|
|||
/Menu.bat
|
||||
/data/
|
||||
screenshots/
|
||||
screenshots-hw/
|
||||
|
|
|
|||
|
|
@ -649,6 +649,13 @@ API Changes
|
|||
- navAgentNew keeps the radius and height at 0.01 or more, as navNew
|
||||
does, so onNavArrived fires for an agent given a radius of zero.
|
||||
|
||||
- materialSetCutoff(material, cutoff) discards the texels whose base
|
||||
colour alpha falls below the cutoff, in the lit pass and in the shadow
|
||||
pass alike, so cutout foliage casts cutout shadows. glTF's MASK alpha
|
||||
mode now sets it from the file's alphaCutoff instead of warning that
|
||||
the material would be drawn opaque; BLEND and OPAQUE are unchanged.
|
||||
Materials that never set a cutoff keep drawing every texel.
|
||||
|
||||
- --deterministic[=MS] runs the engine on a virtual clock stepped MS
|
||||
milliseconds (default 15) once a frame instead of on real time, steps
|
||||
the disc one video frame a frame with it, and seeds Lua's generator
|
||||
|
|
|
|||
17
INSTALL
17
INSTALL
|
|
@ -59,9 +59,20 @@ library (zlib, zstd, SDL3 and its satellites, OpenSSL, FFmpeg) into
|
|||
copied to .builddir/Singe-v<version>-<Os>-<arch>.
|
||||
|
||||
Host packages (what build-all.sh installs): build-essential cmake
|
||||
pkg-config perl nasm llvm imagemagick lua5.4 ffmpeg asciidoctor
|
||||
ruby-asciidoctor-pdf autoconf automake libtool libasound-dev libxi-dev
|
||||
libvdpau-dev libva-dev libdrm-dev libgl-dev libx11-dev.
|
||||
git-lfs pkg-config perl nasm llvm autoconf automake libtool imagemagick
|
||||
ffmpeg lua5.4 asciidoctor ruby-asciidoctor-pdf libva-dev libvdpau-dev
|
||||
libdrm-dev libgl-dev libegl-dev libgles-dev libgbm-dev libasound2-dev
|
||||
libpulse-dev libpipewire-0.3-dev libjack-jackd2-dev libsndio-dev
|
||||
libudev-dev libdbus-1-dev libibus-1.0-dev libxkbcommon-dev libx11-dev
|
||||
libxext-dev libxfixes-dev libxi-dev libxcursor-dev libxrandr-dev
|
||||
libxss-dev libxtst-dev libwayland-dev wayland-protocols libdecor-0-dev.
|
||||
|
||||
The artwork, the font and the menu video in assets/ are stored with Git
|
||||
LFS. Clone with git-lfs present, or those files arrive as small text
|
||||
pointers and the build embeds the pointers instead of the assets; a
|
||||
checkout already made without it is repaired with "git lfs install &&
|
||||
git lfs pull". build-all.sh stops with an error rather than build a
|
||||
binary around them.
|
||||
|
||||
The Linux release build and the Windows build use zig as the compiler:
|
||||
the build fetches a pinned zig release (version and SHA-256 in
|
||||
|
|
|
|||
176
build-all.sh
176
build-all.sh
|
|
@ -32,6 +32,66 @@
|
|||
# ./build-all.sh linux x86_64 --target rebuild-ffmpeg
|
||||
|
||||
G_BUILDDIR=.builddir
|
||||
G_INSTALL="$(dirname "$0")/INSTALL"
|
||||
|
||||
# The one copy of the host package list. Everything else that names these packages derives from it.
|
||||
# Grouped by what wants them, because the SDL3 half is long and a bare list gives no clue why a
|
||||
# package is here: SDL3 compiles a backend whenever it finds the headers, so a package missing from
|
||||
# this list does not fail the build, it quietly drops a backend from the binary.
|
||||
G_HOSTPACKAGES=(
|
||||
# Toolchain
|
||||
build-essential
|
||||
cmake
|
||||
git-lfs
|
||||
pkg-config
|
||||
perl
|
||||
nasm
|
||||
llvm
|
||||
autoconf
|
||||
automake
|
||||
libtool
|
||||
# Content the build generates: embedded images, the menu video, the LuaSec table, the manual
|
||||
imagemagick
|
||||
ffmpeg
|
||||
lua5.4
|
||||
asciidoctor
|
||||
ruby-asciidoctor-pdf
|
||||
# Video decode and display
|
||||
libva-dev
|
||||
libvdpau-dev
|
||||
libdrm-dev
|
||||
libgl-dev
|
||||
libegl-dev
|
||||
libgles-dev
|
||||
libgbm-dev
|
||||
# SDL3 audio backends
|
||||
libasound2-dev
|
||||
libpulse-dev
|
||||
libpipewire-0.3-dev
|
||||
libjack-jackd2-dev
|
||||
libsndio-dev
|
||||
# SDL3 device hotplug, desktop integration and input methods
|
||||
libudev-dev
|
||||
libdbus-1-dev
|
||||
libibus-1.0-dev
|
||||
libxkbcommon-dev
|
||||
# SDL3 X11 video backend and the extensions it uses
|
||||
libx11-dev
|
||||
libxext-dev
|
||||
libxfixes-dev
|
||||
libxi-dev
|
||||
libxcursor-dev
|
||||
libxrandr-dev
|
||||
libxss-dev
|
||||
libxtst-dev
|
||||
# SDL3 Wayland video backend
|
||||
libwayland-dev
|
||||
wayland-protocols
|
||||
libdecor-0-dev
|
||||
)
|
||||
|
||||
G_INSTALLLEAD="Host packages (what build-all.sh installs):"
|
||||
G_INSTALLWIDTH=72
|
||||
|
||||
|
||||
function buildAll() {
|
||||
|
|
@ -54,37 +114,107 @@ function buildAll() {
|
|||
}
|
||||
|
||||
|
||||
# assets/ holds the artwork, the font and the menu video in Git LFS. A clone made without git-lfs
|
||||
# leaves pointer stubs -- a hundred and thirty bytes of text where a video should be -- and the
|
||||
# resource generators embed them without complaining, so the failure only shows up at run time as a
|
||||
# menu with no background. One sentinel is enough to tell the two apart.
|
||||
function checkoutHydrated() {
|
||||
local SENTINEL="$(dirname "$0")/assets/180503_01_PurpleGrid.mp4"
|
||||
|
||||
# A sentinel that is not there at all is not a hydrated checkout either, and saying so beats
|
||||
# passing quietly because the path was wrong.
|
||||
[[ -f "${SENTINEL}" ]] || return 1
|
||||
! head -c 64 "${SENTINEL}" | grep -q "git-lfs.github.com/spec"
|
||||
}
|
||||
|
||||
|
||||
# The package list as INSTALL wants to read it: one sentence, wrapped, ending in a full stop.
|
||||
function installParagraph() {
|
||||
local LINE="${G_INSTALLLEAD}"
|
||||
local SEP=" "
|
||||
local WORD
|
||||
|
||||
for WORD in "${G_HOSTPACKAGES[@]}"; do
|
||||
if (( ${#LINE} + ${#SEP} + ${#WORD} > G_INSTALLWIDTH )); then
|
||||
printf '%s\n' "${LINE}"
|
||||
LINE="${WORD}"
|
||||
else
|
||||
LINE="${LINE}${SEP}${WORD}"
|
||||
fi
|
||||
SEP=" "
|
||||
done
|
||||
printf '%s.\n' "${LINE}"
|
||||
}
|
||||
|
||||
|
||||
# What INSTALL says today: from the lead line to the first line that ends the sentence.
|
||||
function installCurrent() {
|
||||
awk -v lead="${G_INSTALLLEAD}" '
|
||||
index($0, lead) == 1 { found = 1 }
|
||||
found { print; if (/\.$/) exit }
|
||||
' "${G_INSTALL}"
|
||||
}
|
||||
|
||||
|
||||
function installMatches() {
|
||||
# A checkout without INSTALL is not worth nagging about; there is nothing to disagree with.
|
||||
[[ ! -f "${G_INSTALL}" ]] || [[ "$(installParagraph)" == "$(installCurrent)" ]]
|
||||
}
|
||||
|
||||
|
||||
# Writes the paragraph back into INSTALL, replacing whatever sentence is there now.
|
||||
function installSync() {
|
||||
local TEMP
|
||||
|
||||
TEMP=$(mktemp)
|
||||
awk -v lead="${G_INSTALLLEAD}" -v replacement="$(installParagraph)" '
|
||||
index($0, lead) == 1 && !done {
|
||||
print replacement
|
||||
skip = 1
|
||||
}
|
||||
skip { if (/\.$/) { skip = 0; done = 1 } next }
|
||||
{ print }
|
||||
' "${G_INSTALL}" > "${TEMP}"
|
||||
mv "${TEMP}" "${G_INSTALL}"
|
||||
}
|
||||
|
||||
|
||||
# -e = stop script on errors
|
||||
# -u = stop script on undefined variable
|
||||
# -o pipefail = stop pipeline if any step fails
|
||||
set -euo pipefail
|
||||
|
||||
case "${1:-}" in
|
||||
--packages)
|
||||
printf '%s\n' "${G_HOSTPACKAGES[@]}"
|
||||
exit 0
|
||||
;;
|
||||
--sync-install)
|
||||
installSync
|
||||
echo "INSTALL rewritten from build-all.sh."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p ${G_BUILDDIR}
|
||||
|
||||
|
||||
# Host packages the build needs on a Debian based system (the same list is in INSTALL).
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
pkg-config \
|
||||
perl \
|
||||
nasm \
|
||||
llvm \
|
||||
imagemagick \
|
||||
lua5.4 \
|
||||
ffmpeg \
|
||||
asciidoctor \
|
||||
ruby-asciidoctor-pdf \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool \
|
||||
libasound-dev \
|
||||
libxi-dev \
|
||||
libvdpau-dev \
|
||||
libva-dev \
|
||||
libdrm-dev \
|
||||
libgl-dev \
|
||||
libx11-dev
|
||||
# Host packages the build needs on a Debian based system. INSTALL prints the same list for a
|
||||
# reader who has not run anything yet, so it is written from this one: --sync-install rewrites
|
||||
# that paragraph and every build checks it, which is why there is no second list to keep in step.
|
||||
if ! installMatches; then
|
||||
echo "warning: INSTALL's host package list no longer matches this script."
|
||||
echo " Run ./build-all.sh --sync-install to write it from here."
|
||||
fi
|
||||
sudo apt-get install -y "${G_HOSTPACKAGES[@]}"
|
||||
|
||||
|
||||
if ! checkoutHydrated; then
|
||||
echo "error: assets/ holds git-lfs pointer files, not the real artwork, font and video."
|
||||
echo " The build would embed those stubs and produce a broken binary."
|
||||
echo " Fix the checkout with: git lfs install && git lfs pull"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
if [[ $# -ge 2 ]]; then
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
# Invoked in script mode: -DSHADERCROSS -DSOURCE=<file.hlsl> -DOUTPUT=<header> -DENTRIES=<name:stage;...>
|
||||
# -DPREFIX=<lowerCamel prefix> -DTYPE=<struct name>. ENTRIES, PREFIX and TYPE default to the scene's.
|
||||
if(NOT ENTRIES)
|
||||
set(ENTRIES vertexStatic:vertex vertexSkinned:vertex fragmentMain:fragment depthMain:fragment particleVertex:vertex particleFragment:fragment lineVertex:vertex lineFragment:fragment postVertex:vertex postFragment:fragment skyFragment:fragment bloomDown:fragment bloomUp:fragment)
|
||||
set(ENTRIES vertexStatic:vertex vertexSkinned:vertex fragmentMain:fragment depthMain:fragment depthCutoutMain:fragment particleVertex:vertex particleFragment:fragment lineVertex:vertex lineFragment:fragment postVertex:vertex postFragment:fragment skyFragment:fragment bloomDown:fragment bloomUp:fragment)
|
||||
endif()
|
||||
if(NOT PREFIX)
|
||||
set(PREFIX sceneShader)
|
||||
|
|
|
|||
|
|
@ -1976,6 +1976,14 @@ Beyond that:
|
|||
* `materialSetBlend` makes the alpha count, for glass, ghosts and water.
|
||||
Blended meshes draw after everything opaque, sorted by distance, and do
|
||||
not cast shadows.
|
||||
* `materialSetCutoff` keeps only the texels whose alpha reaches the
|
||||
cutoff and throws the rest away, which is how leaves, fences, grates
|
||||
and chain link are drawn: one quad, most of it gone. Unlike blending it
|
||||
needs no sorting and it still casts a shadow, and the shadow is cut out
|
||||
too, so a tree throws leaf shadows rather than the shadow of its quad.
|
||||
A cutoff of 0 (the default) turns masking off. A model loaded from glTF
|
||||
brings its own: `MASK` sets the cutoff from the file, `BLEND` turns
|
||||
blending on, and `OPAQUE` does neither.
|
||||
|
||||
The texture of a material can be any loaded sprite's image
|
||||
(`materialSetTexture`), the laserdisc itself or a loaded video
|
||||
|
|
|
|||
|
|
@ -854,9 +854,10 @@ static int32_t _loadMaterial(ImageCacheT *cache, const cgltf_material *material)
|
|||
_applyTexture(handle, cache, &material->occlusion_texture, MAP_OCCLUSION, material->occlusion_texture.scale);
|
||||
_applyTexture(handle, cache, &material->emissive_texture, MAP_EMISSIVE, 1.0f);
|
||||
materialSetEmissiveLinear(handle, material->emissive_factor[0] * strength, material->emissive_factor[1] * strength, material->emissive_factor[2] * strength);
|
||||
if (material->alpha_mode == cgltf_alpha_mode_mask) {
|
||||
_warn("Material %s uses alpha masking (cutoff %.2f), which is drawn opaque.", material->name ? material->name : "(unnamed)", material->alpha_cutoff);
|
||||
}
|
||||
// glTF's three alpha modes: OPAQUE ignores alpha, BLEND sorts and blends, MASK keeps the
|
||||
// texels at or above a cutoff and discards the rest. A cutoff of zero is masking that keeps
|
||||
// everything, so the mode decides whether one is set at all rather than the value.
|
||||
materialSetCutoff(handle, (material->alpha_mode == cgltf_alpha_mode_mask) ? material->alpha_cutoff : 0.0f);
|
||||
materialSetBlend(handle, material->alpha_mode == cgltf_alpha_mode_blend);
|
||||
materialSetDoubleSided(handle, material->double_sided);
|
||||
materialSetUnlit(handle, material->unlit);
|
||||
|
|
|
|||
53
src/scene.c
53
src/scene.c
|
|
@ -65,9 +65,10 @@
|
|||
#define PIPELINE_SKINNED 1
|
||||
#define PIPELINE_BLEND 2
|
||||
#define PIPELINE_TWO_SIDED 4
|
||||
#define SHADOW_PIPELINES 4 // skinned x double sided
|
||||
#define SHADOW_PIPELINES 8 // skinned x double sided x cutout
|
||||
#define SHADOW_PIPELINE_SKINNED 1
|
||||
#define SHADOW_PIPELINE_TWO_SIDED 2
|
||||
#define SHADOW_PIPELINE_CUTOUT 4
|
||||
#define SAMPLE_SETS 2 // Pipelines per target sample count ...
|
||||
#define SAMPLE_SET_SINGLE 0 // ... single sample (views, and the window without antialiasing) ...
|
||||
#define SAMPLE_SET_MULTI 1 // ... and the window's multisampled targets
|
||||
|
|
@ -166,7 +167,7 @@ typedef struct MaterialUniformsS {
|
|||
float baseColor[4];
|
||||
float emissive[4];
|
||||
float material[4]; // x = metallic, y = roughness, z = unlit (1/0), w = TEXTURE_*
|
||||
float maps[4]; // x = normal map strength (0 = none), y = occlusion strength (0 = none)
|
||||
float maps[4]; // x = normal map strength (0 = none), y = occlusion strength (0 = none), z = alpha cutoff (0 = none)
|
||||
float tiling[4]; // x, y = texture repeats across the surface
|
||||
} MaterialUniformsT;
|
||||
|
||||
|
|
@ -225,6 +226,7 @@ typedef struct MaterialS {
|
|||
Vec3T emissive;
|
||||
float metallic;
|
||||
float roughness;
|
||||
float cutoff; // glTF alpha masking: texels below this are discarded, 0 = off
|
||||
SDL_GPUTexture *texture; // Owned; NULL means untextured
|
||||
SDL_GPUTexture *normalMap; // Owned, each NULL when absent
|
||||
SDL_GPUTexture *occlusionMap;
|
||||
|
|
@ -448,6 +450,7 @@ typedef struct SceneS {
|
|||
SDL_GPUShader *vertexSkinned;
|
||||
SDL_GPUShader *fragment;
|
||||
SDL_GPUShader *depthFragment; // Empty; the shadow pass writes depth only
|
||||
SDL_GPUShader *depthCutoutFragment; // Samples the base texture to mask, for cutout casters
|
||||
SDL_GPUShader *particleVertex;
|
||||
SDL_GPUShader *particleFragment;
|
||||
SDL_GPUShader *lineVertex;
|
||||
|
|
@ -1307,6 +1310,7 @@ static bool _createShaders(void) {
|
|||
_scene.vertexSkinned = _createShader(&sceneShaderVertexSkinned, SDL_GPU_SHADERSTAGE_VERTEX, 0, 2, 2);
|
||||
_scene.fragment = _createShader(&sceneShaderFragmentMain, SDL_GPU_SHADERSTAGE_FRAGMENT, MATERIAL_SAMPLERS, 2, 0);
|
||||
_scene.depthFragment = _createShader(&sceneShaderDepthMain, SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 0, 0);
|
||||
_scene.depthCutoutFragment = _createShader(&sceneShaderDepthCutoutMain, SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 2, 0);
|
||||
_scene.particleVertex = _createShader(&sceneShaderParticleVertex, SDL_GPU_SHADERSTAGE_VERTEX, 0, 1, 0);
|
||||
_scene.particleFragment = _createShader(&sceneShaderParticleFragment, SDL_GPU_SHADERSTAGE_FRAGMENT, 2, 2, 0);
|
||||
_scene.lineVertex = _createShader(&sceneShaderLineVertex, SDL_GPU_SHADERSTAGE_VERTEX, 0, 1, 0);
|
||||
|
|
@ -1316,7 +1320,7 @@ static bool _createShaders(void) {
|
|||
_scene.postFragment = _createShader(&sceneShaderPostFragment, SDL_GPU_SHADERSTAGE_FRAGMENT, 2, 1, 0);
|
||||
_scene.bloomDownFragment = _createShader(&sceneShaderBloomDown, SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1, 0);
|
||||
_scene.bloomUpFragment = _createShader(&sceneShaderBloomUp, SDL_GPU_SHADERSTAGE_FRAGMENT, 2, 1, 0);
|
||||
return (_scene.vertexStatic != NULL) && (_scene.vertexSkinned != NULL) && (_scene.fragment != NULL) && (_scene.depthFragment != NULL) && (_scene.particleVertex != NULL) && (_scene.particleFragment != NULL) && (_scene.postVertex != NULL) && (_scene.postFragment != NULL) && (_scene.skyFragment != NULL) && (_scene.bloomDownFragment != NULL) && (_scene.bloomUpFragment != NULL) && (_scene.lineVertex != NULL) && (_scene.lineFragment != NULL);
|
||||
return (_scene.vertexStatic != NULL) && (_scene.vertexSkinned != NULL) && (_scene.fragment != NULL) && (_scene.depthFragment != NULL) && (_scene.depthCutoutFragment != NULL) && (_scene.particleVertex != NULL) && (_scene.particleFragment != NULL) && (_scene.postVertex != NULL) && (_scene.postFragment != NULL) && (_scene.skyFragment != NULL) && (_scene.bloomDownFragment != NULL) && (_scene.bloomUpFragment != NULL) && (_scene.lineVertex != NULL) && (_scene.lineFragment != NULL);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1367,7 +1371,7 @@ static bool _createShadowPipeline(int32_t variant) {
|
|||
memset(&info, 0, sizeof(info));
|
||||
_describeMeshVertex(&buffer, attributes);
|
||||
info.vertex_shader = (variant & SHADOW_PIPELINE_SKINNED) ? _scene.vertexSkinned : _scene.vertexStatic;
|
||||
info.fragment_shader = _scene.depthFragment;
|
||||
info.fragment_shader = (variant & SHADOW_PIPELINE_CUTOUT) ? _scene.depthCutoutFragment : _scene.depthFragment;
|
||||
info.vertex_input_state.vertex_buffer_descriptions = &buffer;
|
||||
info.vertex_input_state.num_vertex_buffers = 1;
|
||||
info.vertex_input_state.vertex_attributes = attributes;
|
||||
|
|
@ -1805,7 +1809,7 @@ static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, i
|
|||
continue;
|
||||
}
|
||||
// A bulb inside a closed mesh sees only its back faces; they must still cast.
|
||||
variant = ((variant & PIPELINE_SKINNED) ? SHADOW_PIPELINE_SKINNED : 0) | ((twoSided || (variant & PIPELINE_TWO_SIDED)) ? SHADOW_PIPELINE_TWO_SIDED : 0);
|
||||
variant = ((variant & PIPELINE_SKINNED) ? SHADOW_PIPELINE_SKINNED : 0) | ((twoSided || (variant & PIPELINE_TWO_SIDED)) ? SHADOW_PIPELINE_TWO_SIDED : 0) | ((material->cutoff > 0.0f) ? SHADOW_PIPELINE_CUTOUT : 0);
|
||||
if ((_scene.shadowPipelines[variant] == NULL) && !_createShadowPipeline(variant)) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1882,6 +1886,28 @@ static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, i
|
|||
if (_scene.draws[index].skin != NO_HANDLE) {
|
||||
SDL_PushGPUVertexUniformData(commands, SKIN_UNIFORMS, &_scene.skins[_scene.draws[index].skin], sizeof(SkinUniformsT));
|
||||
}
|
||||
// A cutout caster binds what the masking depth shader reads and nothing else: the base
|
||||
// texture, the colour whose alpha it multiplies, the cutoff and the tiling. The shader
|
||||
// declares the frame's buffer it never reads, so a zeroed one goes in to fill the slot.
|
||||
if (shadowPass && ((variant & SHADOW_PIPELINE_CUTOUT) != 0)) {
|
||||
SDL_GPUTextureSamplerBinding cutoutBinding;
|
||||
FragmentUniformsT unread;
|
||||
|
||||
memset(&unread, 0, sizeof(unread));
|
||||
memset(&materialUniforms, 0, sizeof(materialUniforms));
|
||||
baseTexture = _materialTexture(material);
|
||||
materialUniforms.baseColor[3] = material->baseColor.w;
|
||||
materialUniforms.material[3] = (float)((baseTexture == NULL) ? TEXTURE_NONE : TEXTURE_SRGB);
|
||||
materialUniforms.maps[2] = material->cutoff;
|
||||
materialUniforms.tiling[0] = material->tilingU;
|
||||
materialUniforms.tiling[1] = material->tilingV;
|
||||
memset(&cutoutBinding, 0, sizeof(cutoutBinding));
|
||||
cutoutBinding.texture = (baseTexture != NULL) ? baseTexture : _scene.white;
|
||||
cutoutBinding.sampler = (material->filter == FILTER_NEAREST) ? _scene.nearestSampler : _scene.sampler;
|
||||
SDL_PushGPUFragmentUniformData(commands, FRAME_UNIFORMS, &unread, sizeof(unread));
|
||||
SDL_PushGPUFragmentUniformData(commands, MATERIAL_UNIFORMS, &materialUniforms, sizeof(materialUniforms));
|
||||
SDL_BindGPUFragmentSamplers(pass, 0, &cutoutBinding, 1);
|
||||
}
|
||||
if (!shadowPass) {
|
||||
baseTexture = _materialTexture(material);
|
||||
memset(&materialUniforms, 0, sizeof(materialUniforms));
|
||||
|
|
@ -1899,6 +1925,7 @@ static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, i
|
|||
materialUniforms.material[3] = (float)((baseTexture == NULL) ? TEXTURE_NONE : (((material->feed != NO_HANDLE) || (material->view != NO_HANDLE)) ? TEXTURE_FEED : TEXTURE_SRGB));
|
||||
materialUniforms.maps[0] = (material->normalMap != NULL) ? material->normalStrength : 0.0f;
|
||||
materialUniforms.maps[1] = (material->occlusionMap != NULL) ? material->occlusionStrength : 0.0f;
|
||||
materialUniforms.maps[2] = material->cutoff;
|
||||
materialUniforms.tiling[0] = material->tilingU;
|
||||
materialUniforms.tiling[1] = material->tilingV;
|
||||
SDL_PushGPUFragmentUniformData(commands, MATERIAL_UNIFORMS, &materialUniforms, sizeof(materialUniforms));
|
||||
|
|
@ -3945,6 +3972,18 @@ bool materialSetBlend(int32_t material, bool blend) {
|
|||
}
|
||||
|
||||
|
||||
// glTF's alpha masking: a texel whose base colour alpha falls below the cutoff is discarded, in
|
||||
// the lit pass and in the shadow pass alike. Zero turns masking off, which is the default and what
|
||||
// every material that never asked for it keeps.
|
||||
bool materialSetCutoff(int32_t material, float cutoff) {
|
||||
if (!materialValid(material)) {
|
||||
return false;
|
||||
}
|
||||
_scene.materials[material].cutoff = SDL_clamp(cutoff, 0.0f, 1.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool materialSetColor(int32_t material, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
|
||||
if (!materialValid(material)) {
|
||||
return false;
|
||||
|
|
@ -5634,6 +5673,10 @@ void sceneQuit(void) {
|
|||
if (_scene.fragment != NULL) {
|
||||
SDL_ReleaseGPUShader(_scene.device, _scene.fragment);
|
||||
}
|
||||
if (_scene.depthCutoutFragment != NULL) {
|
||||
SDL_ReleaseGPUShader(_scene.device, _scene.depthCutoutFragment);
|
||||
_scene.depthCutoutFragment = NULL;
|
||||
}
|
||||
if (_scene.depthFragment != NULL) {
|
||||
SDL_ReleaseGPUShader(_scene.device, _scene.depthFragment);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ int32_t materialNew(void);
|
|||
bool materialSetBlend(int32_t material, bool blend);
|
||||
bool materialSetColor(int32_t material, uint8_t r, uint8_t g, uint8_t b, uint8_t a);
|
||||
bool materialSetColorLinear(int32_t material, float r, float g, float b, float a);
|
||||
bool materialSetCutoff(int32_t material, float cutoff);
|
||||
bool materialSetDoubleSided(int32_t material, bool doubleSided);
|
||||
bool materialSetEmissive(int32_t material, uint8_t r, uint8_t g, uint8_t b);
|
||||
bool materialSetEmissiveLinear(int32_t material, float r, float g, float b);
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ cbuffer MaterialUniforms : register(b1, space3) {
|
|||
float4 baseColor;
|
||||
float4 emissive;
|
||||
float4 material; // x = metallic, y = roughness, z = unlit (1/0), w = TEXTURE_*
|
||||
float4 maps; // x = normal map strength (0 = none), y = occlusion strength (0 = none)
|
||||
float4 maps; // x = normal map strength (0 = none), y = occlusion strength (0 = none), z = alpha cutoff (0 = none)
|
||||
float4 tiling; // x, y = texture repeats across the surface
|
||||
};
|
||||
|
||||
|
|
@ -372,6 +372,22 @@ void depthMain(VertexOutput input) {
|
|||
}
|
||||
|
||||
|
||||
// The shadow pass for a material with an alpha cutoff. Depth only as well, but the texel has to be
|
||||
// tested or a leaf casts the shadow of the quad it is drawn on. A video or view feed never masks,
|
||||
// so the plain sample covers both texture cases; only the alpha is read either way.
|
||||
void depthCutoutMain(VertexOutput input) {
|
||||
float4 albedo = baseColor;
|
||||
float2 uv = input.uv * tiling.xy;
|
||||
|
||||
if (material.w > 0.5) {
|
||||
albedo *= baseTexture.Sample(baseSampler, uv);
|
||||
}
|
||||
if ((maps.z > 0.0) && (albedo.a < maps.z)) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Distance fog toward the fog colour between near and far, when it is on.
|
||||
float3 applyFog(float3 colour, float3 worldPosition) {
|
||||
float d;
|
||||
|
|
@ -488,6 +504,11 @@ float4 fragmentMain(VertexOutput input) : SV_Target {
|
|||
} else if (material.w > 0.5) {
|
||||
albedo *= baseTexture.Sample(baseSampler, uv);
|
||||
}
|
||||
// Alpha masking: a glTF MASK material keeps only the texels at or above its cutoff. Tested
|
||||
// before any lighting, so a discarded texel costs nothing beyond the sample above.
|
||||
if ((maps.z > 0.0) && (albedo.a < maps.z)) {
|
||||
discard;
|
||||
}
|
||||
if (material.z > 0.5) {
|
||||
return float4(applyFog(albedo.rgb, input.worldPosition), albedo.a);
|
||||
}
|
||||
|
|
|
|||
10
src/singe.c
10
src/singe.c
|
|
@ -1004,6 +1004,7 @@ static int32_t apiMaterialDelete(lua_State *L);
|
|||
static int32_t apiMaterialNew(lua_State *L);
|
||||
static int32_t apiMaterialSetBlend(lua_State *L);
|
||||
static int32_t apiMaterialSetColor(lua_State *L);
|
||||
static int32_t apiMaterialSetCutoff(lua_State *L);
|
||||
static int32_t apiMaterialSetDoubleSided(lua_State *L);
|
||||
static int32_t apiMaterialSetEmissive(lua_State *L);
|
||||
static int32_t apiMaterialSetEmissiveMap(lua_State *L);
|
||||
|
|
@ -4999,6 +5000,7 @@ static void _registerApi(lua_State *L) {
|
|||
lua_register(L, "materialNew", apiMaterialNew); // 3.00
|
||||
lua_register(L, "materialSetBlend", apiMaterialSetBlend); // 3.00
|
||||
lua_register(L, "materialSetColor", apiMaterialSetColor); // 3.00
|
||||
lua_register(L, "materialSetCutoff", apiMaterialSetCutoff); // 3.00
|
||||
lua_register(L, "materialSetDoubleSided", apiMaterialSetDoubleSided); // 3.00
|
||||
lua_register(L, "materialSetEmissive", apiMaterialSetEmissive); // 3.00
|
||||
lua_register(L, "materialSetEmissiveMap", apiMaterialSetEmissiveMap); // 3.00
|
||||
|
|
@ -8868,6 +8870,14 @@ static int32_t apiMaterialSetColor(lua_State *L) {
|
|||
}
|
||||
|
||||
|
||||
// materialSetCutoff(material, cutoff)
|
||||
static int32_t apiMaterialSetCutoff(lua_State *L) {
|
||||
_argCheck(L, "materialSetCutoff", 2, 2);
|
||||
materialSetCutoff(_argMaterial(L, "materialSetCutoff", 1), (float)_argNumber(L, "materialSetCutoff", 2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// materialSetDoubleSided(material, bool)
|
||||
static int32_t apiMaterialSetDoubleSided(lua_State *L) {
|
||||
_argCheck(L, "materialSetDoubleSided", 2, 2);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue