Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ repositories {
name = "Fabric"
url = "https://maven.fabricmc.net/"
}
maven {
name = "Modrinth"
url = "https://api.modrinth.com/maven"
content {
includeGroup "maven.modrinth"
}
}
mavenCentral()
}

Expand Down Expand Up @@ -59,6 +66,11 @@ dependencies {
minecraft "com.mojang:minecraft:${project.minecraft_version}"
implementation "net.fabricmc:fabric-loader:${project.loader_version}"
implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"

// FirstPerson Model, referenced only by the optional compat bridge for its render-state marker
// interface. Compile-only: the mod is neither bundled nor required at runtime, and the coordinate
// pins the Modrinth version id because the plain version number is shared across loaders.
compileOnly "maven.modrinth:first-person-model:6sgz2HEq"
testImplementation platform("org.junit:junit-bom:5.12.2")
testImplementation "org.junit.jupiter:junit-jupiter"
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
Expand Down
42 changes: 34 additions & 8 deletions shaders/pipelines/world/any_hit.rahit.slang
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ static const float ENTITY_ALPHA_CUTOFF = 0.1; // entities: only discard near-ful
// tinted pane still absorbs its own color on top of this.
static const float TRANSLUCENT_NEUTRAL_EXTINCTION = 0.15;
static const float WATER_SHADOW_TINT = 0.5;
// Neutral per-interface shadow attenuation for ice-family terrain (MATERIAL_FEATURE_ICE): strong enough
// that one block of ice (two interfaces, 0.15 squared) reads as near-opaque, removing the separated
// second shadow and blue cast that colored transmission produced, while stacked ice keeps darkening
// monotonically. Traversal still continues, so occluders behind the ice shadow normally.
static const float ICE_SHADOW_TRANSMITTANCE = 0.15;

// Progressive 8x8 blue-noise-style alpha threshold pattern, with a golden-ratio temporal rotation. This keeps
// translucent entity hits stochastic, but trades per-pixel white noise for a stable spatial distribution
Expand Down Expand Up @@ -83,6 +88,20 @@ void main(inout Payload payload, in BuiltInTriangleIntersectionAttributes attr)
// Radiance rays accept the surface and continue to closest-hit. Shadow rays use the same material
// model here so entity dielectrics transmit instead of becoming opaque shadow blockers.
bool shadowRay = (RayFlags() & RAY_FLAG_SKIP_CLOSEST_HIT_SHADER) != 0u;
// The first-person world stand-in is semi-transmissive to shadow rays: one neutral multiply by
// the configured transmittance per ray, however many of its layers the ray crosses. roughMetal
// is the once-marker — the shadow path never consumes it and guide.rmiss never writes it.
// Traversal always continues, so opaque geometry behind the body still blackens the ray in any
// intersection order, and the flags sentinel stays untouched.
if (instanceKind == ENTITY_BIT && shadowRay
&& (g.reserved.x & ENTITY_GEOM_WORLD_STAND_IN) != 0u) {
if (payload.roughMetal == 0u) {
packAlbedo(payload, unpackAlbedo(payload)
* ConstPtr<WorldPush>(pc.worldPushAddr)[0].shadowPolicy.x);
payload.roughMetal = 1u;
}
IgnoreHit();
}
if (instanceKind == ENTITY_BIT && shadowRay && materialHeader.model == MATERIAL_DIELECTRIC) {
float3 tint709 = lerp(float3(1.0),
srgbToLinear(texel.rgb) * srgbToLinear(epr.tint.rgb), texel.a);
Expand Down Expand Up @@ -131,14 +150,21 @@ void main(inout Payload payload, in BuiltInTriangleIntersectionAttributes attr)
if (bucket == BUCKET_TRANSLUCENT) {
TerrainPrim pr = ConstPtr<TerrainPrim>(sec.primAddr)[tri];
MaterialHeader materialHeader = ConstPtr<MaterialHeader>(pc.materialTableAddr)[pr.materialId];
float3 avgColor = max(bt709ToAcesCg(
materialHeader.average.rgb * srgbToLinear(pr.tint.rgb)), float3(1.0e-3));
float3 colorExtinction = max(-log(avgColor), float3(0.0, 0.0, 0.0));
// The neutral floor is a flat per-hit dimming, NOT scaled by alpha: vanilla clear glass has a low
// natural alpha, so folding it into the alpha-scaled term crushed it to near-zero for exactly the
// white/clear-glass case it's meant to cover.
packAlbedo(payload, unpackAlbedo(payload)
* exp(-colorExtinction * materialHeader.average.a - TRANSLUCENT_NEUTRAL_EXTINCTION));
// Ice replaces its colored Beer-Lambert tint with the fixed neutral attenuation, applied per
// interface with no marker and no termination; every other translucent material keeps the
// colored path below unchanged.
if ((materialHeader.features & MATERIAL_FEATURE_ICE) != 0u) {
packAlbedo(payload, unpackAlbedo(payload) * ICE_SHADOW_TRANSMITTANCE);
} else {
float3 avgColor = max(bt709ToAcesCg(
materialHeader.average.rgb * srgbToLinear(pr.tint.rgb)), float3(1.0e-3));
float3 colorExtinction = max(-log(avgColor), float3(0.0, 0.0, 0.0));
// The neutral floor is a flat per-hit dimming, NOT scaled by alpha: vanilla clear glass has a
// low natural alpha, so folding it into the alpha-scaled term crushed it to near-zero for
// exactly the white/clear-glass case it's meant to cover.
packAlbedo(payload, unpackAlbedo(payload)
* exp(-colorExtinction * materialHeader.average.a - TRANSLUCENT_NEUTRAL_EXTINCTION));
}
IgnoreHit();
}

Expand Down
25 changes: 19 additions & 6 deletions shaders/pipelines/world/closest_hit.rchit.slang
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,19 @@ void payloadSetPacked(inout Payload payload, uint material, float roughness, flo
payload.iorTransmission = packHalf2(float2(ior, transmission));
}

// Which side of a dielectric face this hit is on, shared by every hit path. Comes from the face
// orientation, so it is re-derived at each crossing instead of toggled (see PAYLOAD_DIELECTRIC_ENTERING).
void payloadSetDielectric(inout Payload payload, uint material, bool entering) {
// Which side of a dielectric face this hit is on, plus the volume's canonical medium identity and ice
// marker, shared by every hit path. The side comes from the face orientation, so it is re-derived at
// each crossing instead of toggled (see PAYLOAD_DIELECTRIC_ENTERING).
void payloadSetDielectric(inout Payload payload, uint material, bool entering,
uint materialId, uint features) {
if (material != MATERIAL_WATER && material != MATERIAL_DIELECTRIC) return;
if (entering) payload.flags |= PAYLOAD_DIELECTRIC_ENTERING;
uint mediumId = material == MATERIAL_WATER
? MEDIUM_ID_WATER : materialId + MEDIUM_ID_DIELECTRIC_BASE;
payload.flags |= (mediumId << PAYLOAD_MEDIUM_ID_SHIFT) & PAYLOAD_MEDIUM_ID_MASK;
if ((features & MATERIAL_FEATURE_ICE) != 0u) {
payload.flags |= PAYLOAD_SURFACE_ICE;
}
}

uint materialEmissionSource(MaterialHeader header, float emission) {
Expand Down Expand Up @@ -322,7 +330,11 @@ void main(inout Payload payload, in BuiltInTriangleIntersectionAttributes attr)
payloadSetPacked(payload, material, surface.roughness, surface.metalness,
emission, sss, header.params.z, header.params.w,
materialEmissionSource(header, emission));
payloadSetDielectric(payload, material, entering);
payloadSetDielectric(payload, material, entering, pr.materialId, header.features);
// After payloadSetPacked, which ASSIGNS flags rather than OR-ing into it.
if ((g.reserved.x & ENTITY_GEOM_LOCAL_VIEW) != 0u) {
payload.flags |= PAYLOAD_SURFACE_LOCAL_VIEW;
}
return;
}

Expand Down Expand Up @@ -393,7 +405,8 @@ void main(inout Payload payload, in BuiltInTriangleIntersectionAttributes attr)
payloadSetPacked(payload, MATERIAL_DIELECTRIC, glassSurface.roughness,
glassSurface.metalness, 0.0, 0.0, materialHeader.params.z,
materialHeader.params.w, EMISSION_SOURCE_NONE);
payloadSetDielectric(payload, MATERIAL_DIELECTRIC, entering);
payloadSetDielectric(payload, MATERIAL_DIELECTRIC, entering, pr.materialId,
materialHeader.features);
return;
}

Expand Down Expand Up @@ -430,7 +443,7 @@ void main(inout Payload payload, in BuiltInTriangleIntersectionAttributes attr)
payloadSetPacked(payload, material, surface.roughness, surface.metalness,
surface.emission, surface.sss, materialHeader.params.z, materialHeader.params.w,
materialEmissionSource(materialHeader, surface.emission));
payloadSetDielectric(payload, material, entering);
payloadSetDielectric(payload, material, entering, pr.materialId, materialHeader.features);
// RIS emitter-NEE membership: raygen gates this emitter's direct-hit emission term (RIS covers it).
if ((pr.flags & TERRAIN_PRIM_IN_LIGHT_BUFFER) != 0u) {
payload.flags |= PAYLOAD_EMITTER_IN_LIST;
Expand Down
82 changes: 61 additions & 21 deletions shaders/pipelines/world/guides.slang
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,18 @@ public struct SpecSurface {
public float3 motionPrev; // current-minus-previous displacement of the reflecting surface
public float roughness;
public float3 albedo; // specular albedo fed to DLSS-RR for demodulation (0 = pure diffuse)
// Domain for the reflection probe. The interface's own domain, carried here because the probe runs
// from writeGuides — after tracePrimary returned and after the transmission chain has overwritten the
// global payload, at which point the primary hit's domain is no longer recoverable.
public uint secondaryRayMask;
};
public static SpecSurface gv_spec = {};

// Static surfaces reuse one normal for all three roles. Water overrides them (wave-displaced shading
// normal now and last frame, flat geometric normal for the bias) via the six-argument form.
public SpecSurface makeSpecSurface(float3 camRel, float3 normal, float3 previousNormal,
float3 biasNormal, float3 motionPrev, float roughness, float3 albedo) {
float3 biasNormal, float3 motionPrev, float roughness, float3 albedo,
uint secondaryRayMask) {
SpecSurface s;
s.camRel = camRel;
s.normal = normal;
Expand All @@ -51,17 +56,20 @@ public SpecSurface makeSpecSurface(float3 camRel, float3 normal, float3 previous
s.motionPrev = motionPrev;
s.roughness = roughness;
s.albedo = albedo;
s.secondaryRayMask = secondaryRayMask;
return s;
}

// The convenience forms serve the static/sky/particle paths, which are always world surfaces.
public SpecSurface makeSpecSurface(float3 camRel, float3 normal, float roughness, float3 albedo) {
return makeSpecSurface(camRel, normal, normal, normal,
float3(0.0, 0.0, 0.0), roughness, albedo);
float3(0.0, 0.0, 0.0), roughness, albedo, CULL_SECONDARY);
}

public SpecSurface makeSpecSurface(float3 camRel, float3 normal, float3 motionPrev,
float roughness, float3 albedo) {
return makeSpecSurface(camRel, normal, normal, normal, motionPrev, roughness, albedo);
return makeSpecSurface(camRel, normal, normal, normal, motionPrev, roughness, albedo,
CULL_SECONDARY);
}
// angle can land behind the eye even though the reflector itself is comfortably in view.
public float2 projectPrevNdc(float3 worldPos, out bool valid) {
Expand Down Expand Up @@ -107,7 +115,7 @@ public float2 resolveSpecularGuides(SpecSurface surface, float3 primaryDir,

// Pass A stops radiance traversal at the first split, so reflection motion keeps one dedicated,
// deterministic guide probe.
traceGuide(CULL_SECONDARY,
traceGuide(surface.secondaryRayMask,
offsetSurfaceOrigin(surfacePos, surface.biasNormal, specDir, SURF_BIAS),
RAY_TMIN, specDir, 10000.0,
max(length(surface.camRel) * primaryConeSpread, RAY_CONE_MIN_WIDTH),
Expand Down Expand Up @@ -158,18 +166,25 @@ public void setTransmissionGuide(float3 hitCamRel, float3 motionPrev, float3 nor

// Deterministic ordinary guide behind the first transmitted interface. This is guide-only work:
// radiance reflection/transmission continuations are queued at the first split and traced by Pass B.
public void resolveTransmissionGuide(float3 surfacePos, float3 transmittedDir,
public void resolveTransmissionGuide(uint rayMask, float3 surfacePos, float3 transmittedDir,
float3 surfaceBiasNormal, MediumStack medium, float rayBias,
float rayConeWidth, float rayConeSpread, float3 guideFilter) {
if (dot(transmittedDir, transmittedDir) <= 0.0) return;
// With the local view published, the whole chain keeps the union of the camera domain and the
// local-view secondary domain — matching the radiance chain's representation, never mid-switching
// to a mask that could hit the world stand-in. Unset keeps the per-crossing derivation below.
bool localViewPresent = (worldPush.flags & 4u) != 0u;
if (localViewPresent) {
rayMask = CULL_PRIMARY | CULL_LOCAL_VIEW_SECONDARY;
}

float3 direction = normalize(transmittedDir);
float3 ro = offsetSurfaceOrigin(surfacePos, surfaceBiasNormal, direction, rayBias);

// The camera interface consumed bounce 0; the remaining configured bounce budget is the natural
// cap for deterministic guide crossings too.
for (uint crossing = 0u; crossing < worldPush.maxBounces; ++crossing) {
traceGuide(CULL_PRIMARY, ro, RAY_TMIN, direction, 10000.0,
traceGuide(rayMask, ro, RAY_TMIN, direction, 10000.0,
rayConeWidth, rayConeSpread);
if (payload.hitT <= 0.0) {
setTransmissionGuide((ro + direction * 1.0e6) - worldPush.camOffset,
Expand Down Expand Up @@ -206,25 +221,50 @@ public void resolveTransmissionGuide(float3 surfacePos, float3 transmittedDir,
worldPush.waterParams.w, waterFootprint);
}

float transmission = clamp(payloadTransmission(), 0.0, 1.0);
Medium entered = makeDielectricMedium(isWater, max(payloadIor(), 1.0),
payloadAlbedo(), transmission);
float etaT = entering ? entered.ior : medium.outer.ior;
float3 nextDirection = refract(direction, interfaceNormal, medium.current.ior / etaT);
if (dot(nextDirection, nextDirection) <= 0.0) {
// Never let a TIR reflection become ordinary diffuse/depth.
// An ice interface ends the guide chain: depth, position, normal and motion all describe this
// interface, never a blend with the destination behind it. Shaped like the TIR endpoint above.
if (material == MATERIAL_DIELECTRIC && payloadSurfaceIce()) {
setTransmissionGuide(interfacePos - worldPush.camOffset,
isWater ? float3(0.0, 0.0, 0.0) : float3(payload.motionPrev),
interfaceNormal, 0.0, float3(0.0, 0.0, 0.0), false);
float3(payload.motionPrev), interfaceNormal, 0.0,
float3(0.0, 0.0, 0.0), false);
return;
}
if (!isWater && entering) {
guideFilter *= payloadAlbedo();

float transmission = clamp(payloadTransmission(), 0.0, 1.0);
Medium entered = makeDielectricMedium(payloadMediumId(), max(payloadIor(), 1.0),
payloadAlbedo(), transmission);
uint exitMatch = entering ? MEDIUM_EXIT_NO_MATCH : mediumResolveExit(medium, entered.mediumId);
bool opticalEvent = entering || exitMatch == MEDIUM_EXIT_CURRENT;
float3 nextDirection = direction;
if (opticalEvent) {
float etaT = entering ? entered.ior : medium.parent1.ior;
nextDirection = refract(direction, interfaceNormal, medium.current.ior / etaT);
if (dot(nextDirection, nextDirection) <= 0.0) {
// Never let a TIR reflection become ordinary diffuse/depth.
setTransmissionGuide(interfacePos - worldPush.camOffset,
isWater ? float3(0.0, 0.0, 0.0) : float3(payload.motionPrev),
interfaceNormal, 0.0, float3(0.0, 0.0, 0.0), false);
return;
}
if (!isWater && entering) {
guideFilter *= payloadAlbedo();
}
if (entering) {
// A full stack ends the deterministic chain; the tuple keeps its last endpoint, the
// same fail-closed shape as exhausting the crossing budget.
if (!mediumPush(medium, entered)) {
return;
}
} else {
mediumCommitExit(medium, exitMatch);
}
} else if (mediumExitIsDeep(exitMatch)) {
mediumCommitExit(medium, exitMatch);
}
if (entering) {
mediumPush(medium, entered);
} else {
mediumPop(medium);
if (!localViewPresent) {
// Re-derive from THIS crossing, or a chain that leaves the local-view representation and
// enters world glass would keep probing in the local-view domain.
rayMask = CULL_PRIMARY | secondaryMaskForSurface(payload.flags);
}
direction = normalize(nextDirection);
ro = offsetSurfaceOrigin(interfacePos, geometricNormal, direction,
Expand Down
Loading