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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ changes while the renderer is being built.
terrain streaming...), and every sub-screen starts with a "Reset to Defaults" button — plus a
global one on the hub — so experimentation can never strand you on a botched configuration
- Weather-driven lighting: rain and thunderstorms dim the sun/moon and darken the sky
- Volumetric 3D clouds in classic (vanilla-style boxes) or photoreal cumulus form,
with adjustable thickness, opacity and cloud shadows, visible in reflections
- Volumetric 3D clouds in classic (vanilla-style boxes) or photoreal form — the volumetric deck is
ray-marched with multi-scattered light, three optical-depth probes (sun, sky, ground bounce) and
weather-driven cloud types (stratus sheet, cumulus heaps, storm towers), with adjustable thickness,
opacity and cloud shadows, visible in reflections.
See [docs/realistic-volumetric-clouds.md](docs/realistic-volumetric-clouds.md)
- Dedicated Nether and End skyboxes
- OMM (Opacity Micro-Map) + SER (Shader Execution Reordering) optimizations
- Experimental NVIDIA SHaRC-style world-space radiance cache (Spatially Hashed Radiance Cache) to
Expand Down
446 changes: 446 additions & 0 deletions docs/realistic-volumetric-clouds.md

Large diffs are not rendered by default.

1,098 changes: 943 additions & 155 deletions shaders/world/clouds.slang

Large diffs are not rendered by default.

57 changes: 45 additions & 12 deletions src/main/java/dev/comfyfluffy/caustica/CausticaConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -771,23 +771,42 @@ public static final class Composite {
*
* <p>{@code classic} reproduces vanilla's flat, blocky deck: coverage is quantised to the
* 12-block cell grid so the silhouette is genuinely square-edged, and the slab is shaded
* with vanilla's distinct top/side/bottom faces. {@code volumetric} extrudes the same
* coverage map into a ray-marched slab with self-shadowing and forward scattering — the
* look heavy shaderpacks produce, at a real GPU cost.
* with vanilla's distinct top/side/bottom faces. {@code volumetric} ray-marches a modelled
* water-cloud deck instead — a coverage field for where the layer is, a 3D-eroded height
* profile for the shape of each cloud, and a multi-scattering light model with separate sun,
* sky and ground-bounce optical depths. It is the look heavy shaderpacks produce, at a real
* GPU cost (roughly twice the previous volumetric model per pixel; see
* {@code docs/realistic-volumetric-clouds.md} for the budget and the physics).
*
* <p>Both styles read one shared coverage field, so switching does not move the clouds and
* the cloud shadows stay identical between them.
* the cloud shadows stay identical between them. The volumetric style additionally reads the
* weather to pick the cloud GENUS — a closed sheet when overcast, scattered heaps in fair
* weather, convective towers in a thunderstorm — so rain changes the shape of the sky and
* not merely its brightness.
*/
public static final StringSetting CLOUD_STYLE =
string("caustica.rt.cloudStyle", "composite.cloud-style", "classic",
Composite::sanitizeCloudStyle);
/**
* Cloud thickness, 0..1, as a fraction of {@link #CLOUD_MAX_THICKNESS_BLOCKS}.
* Cloud thickness — how much BULK the deck has, 0..1, as a fraction of
* {@link #CLOUD_MAX_THICKNESS_BLOCKS}.
*
* <p>Volumetric: 0 is a flat sheet (the deck collapses to a plane and takes the cheap
* non-marched path); 1 is a deep bank. Classic: the slider scales the HEIGHT of vanilla's
* <p>This is the cloud's size, not its position: it sets how deep the layer is from its base
* to its crown. How far off the ground that base sits is {@link #CLOUD_HEIGHT}, and the two
* are deliberately independent — a deck can be thin and low, or a kilometre of storm cloud
* overhead, without either slider moving the other. That separation matters because clouds
* are no longer a flat texture stretched across the sky (the old PNG plane had exactly one
* number for both concepts): a modelled deck has real depth, and depth is what the light
* transport integrates over.
*
* <p>Volumetric: this slider does NOT apply — how deep a volumetric cloud is belongs to the
* genus model ({@code cloudDeckDepth} in clouds.slang: 64 blocks of sheet, 165 of heap, 210
* of tower), because a global thickness was precisely the knob that made the deck read as a
* rectangle whose look depends on a slider, and the clouds sub-screen replaces the row with
* a greyed-out explanation in that style. Classic: the slider scales the HEIGHT of vanilla's
* authored cell boxes, floored at vanilla's own 4-block extrusion — classic clouds are
* always real boxes with lit tops and shaded sides, never thinner than the game draws them.
* always real boxes with lit tops and shaded sides, never thinner than the game draws them,
* and 0 collapses them to the flat sheet.
*/
public static final FloatSetting CLOUD_THICKNESS =
clampedFloat("caustica.rt.cloudThickness", "composite.cloud-thickness", 0.5f, 0.0f, 1.0f);
Expand All @@ -809,19 +828,33 @@ public static final class Composite {
public static final FloatSetting CLOUD_OPACITY =
clampedFloat("caustica.rt.cloudOpacity", "composite.cloud-opacity", 0.9f, 0.0f, 1.0f);
/**
* World Y the BASE of the cloud deck sits at. Vanilla's clouds sit at 192; the default is
* higher because Caustica's clouds have real thickness and a deck whose base is at vanilla
* height reads as much closer to the ground than vanilla's flat sheet does.
* World Y the BASE of the cloud deck sits at. Vanilla's clouds sit at 192; the default sits
* a little above that because a modelled deck has real depth overhead and a base at exactly
* vanilla height reads as looming over tall terrain.
*
* <p>Position only: this is where the deck's FLOOR is, never how big it is. The volumetric
* march recovers exactly this Y as its slab base (Java pushes the slab's centre, and the
* shader subtracts half the pushed depth), so the floor does not drift with anything else —
* not with the genus depth, not with the classic-only thickness. The deck grows UPWARD from
* it, by as much as its genus says.
*
* <p>Exposed as a slider: with volumetric clouds the deck's distance is a strong part of the
* look, and the right value depends on the world's terrain height and the player's taste.
* The range comfortably spans from just above build height to far overhead.
*/
public static final FloatSetting CLOUD_HEIGHT =
clampedFloat("caustica.rt.cloudHeight", "composite.cloud-height", 320.0f, 128.0f, 1024.0f);
clampedFloat("caustica.rt.cloudHeight", "composite.cloud-height", 224.0f, 128.0f, 1024.0f);
/**
* Fraction of the sky the deck covers in clear weather. Rain drives this toward fully
* overcast on top of whatever is set here (see {@code RtComposite.cloudState}).
*
* <p>In the volumetric style this also picks the cloud GENUS, because that is what coverage
* means in the real sky: a scattered field (low values) is fair-weather cumulus — individual
* heaps with clear air between them and a flat base at the condensation level — while a
* closed sky (above roughly 55%, fully by 92%) is stratocumulus or stratus, a shallow
* continuous sheet that stops developing vertically. So the slider changes the SHAPE of the
* clouds and not merely their density; the classic style quantises the same value against
* vanilla's authored cell map instead.
*/
public static final FloatSetting CLOUD_COVERAGE =
clampedFloat("caustica.rt.cloudCoverage", "composite.cloud-coverage", 0.55f, 0.0f, 1.0f);
Expand Down
45 changes: 36 additions & 9 deletions src/main/java/dev/comfyfluffy/caustica/client/RtVideoOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,11 @@ public static OptionInstance<?>[] pomOptions() {

/**
* The clouds sub-screen rows. {@code styleChanged} reopens the screen when the deck style flips,
* and the classic deck's coverage slider is left out entirely: coverage is baked into the flat
* texture in that style, so showing the row would offer a knob that changes nothing — the
* sub-screen replaces it with {@link #cloudCoverageDisabledHint()}'s greyed-out explanation.
* Volumetric reads the live coverage field, so the slider stays.
* and each style leaves out the one slider it cannot use: the classic deck's coverage is baked
* into its flat texture, and the volumetric deck's depth comes from its genus model rather than
* from the thickness option — showing either row in the wrong style would offer a knob that
* changes nothing, so the sub-screen swaps them for greyed-out explanations
* ({@link #cloudCoverageDisabledHint()}, {@link #cloudThicknessDisabledHint()}).
*/
public static OptionInstance<?>[] cloudOptions(Runnable styleChanged) {
List<OptionInstance<?>> options = new ArrayList<>();
Expand All @@ -210,7 +211,9 @@ public static OptionInstance<?>[] cloudOptions(Runnable styleChanged) {
options.add(cloudCoverage());
}
options.add(cloudHeight());
options.add(cloudThickness());
if ("classic".equals(CausticaConfig.Rt.Composite.CLOUD_STYLE.get())) {
options.add(cloudThickness());
}
options.add(cloudShadowStrength());
options.add(cloudOpacity());
return options.toArray(OptionInstance<?>[]::new);
Expand Down Expand Up @@ -840,6 +843,28 @@ public static Button cloudCoverageDisabledHint() {
return button;
}

/**
* The thickness slider's volumetric-mode placeholder, the mirror image of
* {@link #cloudCoverageDisabledHint()}. How deep a volumetric cloud is belongs to the genus model
* — a fair-weather cumulus stays shallow, a storm tower fills the sky — so the deck derives its
* slab depth from coverage and weather instead of from this slider, and offering the knob in
* volumetric mode would promise a change the shader deliberately does not make. Returns
* {@code null} when the classic style is selected, i.e. "thickness is usable, no placeholder row".
*/
public static Button cloudThicknessDisabledHint() {
if ("classic".equals(CausticaConfig.Rt.Composite.CLOUD_STYLE.get())) {
return null;
}
Button button = Button.builder(
Component.translatable("caustica.options.rt.cloudThickness.volumetricUnavailable"),
clicked -> {})
.width(310).build();
button.active = false;
button.setTooltip(Tooltip.create(Component.translatable(
"caustica.options.rt.cloudThickness.volumetricUnavailable.tooltip")));
return button;
}

/**
* World Y the base of the cloud deck sits at, in blocks.
*
Expand All @@ -864,10 +889,12 @@ private static OptionInstance<Integer> cloudHeight() {
}

/**
* Cloud thickness, as a percentage of the maximum deck depth. Applies to both styles: 0% is a flat
* sheet, 100% is a deep bank you can fly into. Classic clouds become real boxes with lit tops and
* darker sides, the way vanilla's cloud geometry looks; volumetric clouds gain the depth their
* shading needs. Thicker clouds cost more to march.
* Cloud thickness, as a percentage of the maximum deck depth: 0% is a flat sheet, 100% is a deep
* bank you can fly into. A CLASSIC knob — its boxes are extruded by exactly this amount — and
* hidden in volumetric mode, where the deck's depth is the genus model's instead
* ({@code cloudDeckDepth} in clouds.slang): a cloud's own shape deciding how far it develops is
* what makes volumetric clouds read as clouds, and a global thickness fought that at every
* setting. The row is offered only when it changes something.
*/
private static OptionInstance<Integer> cloudThickness() {
return percent("caustica.options.rt.cloudThickness",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,18 @@ public static RtSettingsSubScreen pom(Screen parent) {

/**
* The cloud deck: style, coverage, altitude, thickness, shadow and opacity. Flipping the style
* reopens the screen: the coverage slider only drives the volumetric deck, so in classic mode it
* swaps to {@link RtVideoOptions#cloudCoverageDisabledHint()}'s disabled placeholder (null in
* volumetric mode — the row simply vanishes).
* reopens the screen, because each style has exactly one slider it cannot use and that row swaps
* to a disabled placeholder: coverage only drives the volumetric deck (classic bakes it into its
* texture), and thickness only shapes the classic boxes (the volumetric deck's depth is its genus
* model's). The placeholders are null in the style where the slider works, so the row vanishes.
*/
public static RtSettingsSubScreen clouds(Screen parent) {
return new RtSettingsSubScreen(parent, options(),
Component.translatable("caustica.options.rt.cloudsHeader"),
cloudsSettings(), RtSubScreens::clouds,
reopen -> List.of(RtSettingsSubScreen.Section.of(null, RtVideoOptions.cloudOptions(reopen),
RtVideoOptions.cloudCoverageDisabledHint())));
RtVideoOptions.cloudCoverageDisabledHint(),
RtVideoOptions.cloudThicknessDisabledHint())));
}

/**
Expand Down
26 changes: 17 additions & 9 deletions src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java
Original file line number Diff line number Diff line change
Expand Up @@ -443,25 +443,33 @@ private static Int4 sharcGridOrigin(RtTerrain terrain) {
// snapped the entire cloudscape to a different pattern — clouds visibly changing shape while
// walking, in the volumetric style only.
//
// The wrap must therefore be a whole period in EVERY space the field is sampled in: the base
// octaves, the domain warp, and both billow layers. The binding constraint is the largest octave
// divisor (CLOUD_WARP_DIV = 2.0 in clouds.slang):
// The wrap must therefore be a whole period in EVERY space the field is sampled in: the coverage
// octaves, the turbulent displacement, and both 3D erosion octaves. The binding constraint is the
// largest octave divisor — CLOUD_SHAPE_DIV and CLOUD_WARP_DIV, both 2.0 in clouds.slang:
//
// period = 512 cells * 12 blocks/cell * maxDivisor(2.0) / scale(0.5) = 24576 blocks
//
// Every divisor there is a power of two, so all of these multiplies are exact in binary floating
// point and the wrap identity holds bit-for-bit rather than approximately. Verified: the full
// density function (base octaves + warp + billow) is now identical across a wrap to 0.0.
// point and the wrap identity holds bit-for-bit rather than approximately, and 24576 is an integer
// multiple of every smaller octave's own period (the erosion layers at 12288 and 3072). Verified: the
// full density function (coverage + displacement + 3D erosion) is identical across a wrap to 0.0.
// RtCloudPeriodMirrorTest re-derives this from the shader's constants so the two cannot drift.
private static final double CLOUD_FIELD_PERIOD_BLOCKS = 512.0 * 12.0 * 2.0 / 0.5;
// Vanilla's clouds drift at 0.03 blocks/tick; matched so the sky moves at a familiar speed.
private static final double CLOUD_WIND_BLOCKS_PER_TICK = 0.03;
// Deck thickness at the slider's 100%. Both styles march a real slab now, so this is the depth the
// clouds actually have in the world: at full thickness a bank is tall enough to fly into, while the
// slider at 0 collapses the deck to the old flat plane.
// Deck thickness at the slider's 100%: the BULK of the cloud layer, i.e. how deep the slab the
// shader marches is. This is the cloud's size, never its altitude (that is CLOUD_HEIGHT, which sets
// the base) — a distinction that only exists because the deck stopped being a flat texture.
// At full thickness a bank is deep enough to fly into; at 0 it collapses to the old flat plane.
// Real cumulus is as tall as it is wide, often taller — a bank whose base sits at cloud height can
// easily tower 100+ blocks. 40 was too shallow for the deck to ever read as heaped rather than
// layered, and since extinction is now normalised by the slab depth (CLOUD_REFERENCE_THICKNESS in
// layered, and since extinction is normalised by the slab depth (CLOUD_REFERENCE_THICKNESS in
// clouds.slang) raising this adds VOLUME without making the clouds more opaque.
//
// The weather does not change this number: the player's slider stays the size budget, and the genus
// model in clouds.slang decides how much of the budget a given sky actually uses — storm towers fill
// the slab, fair-weather heaps round off at ~70% of it, an overcast sheet hugs the lower half. So a
// storm reads as deeper cloud without ever overriding the setting.
private static final float CLOUD_MAX_THICKNESS_BLOCKS = 110.0f;
// Classic boxes never get thinner than vanilla's own 4-block extrusion (CloudRenderer's
// putVec3(12, 4, 12)): the thickness slider scales the box HEIGHT from that baseline up, per the
Expand Down
Loading
Loading