diff --git a/common/src/api/java/dev/engine_room/flywheel/api/instance/InstancerProvider.java b/common/src/api/java/dev/engine_room/flywheel/api/instance/InstancerProvider.java
index a921ca61c..d228eea3c 100644
--- a/common/src/api/java/dev/engine_room/flywheel/api/instance/InstancerProvider.java
+++ b/common/src/api/java/dev/engine_room/flywheel/api/instance/InstancerProvider.java
@@ -17,13 +17,13 @@ public interface InstancerProvider {
*
Render Order
* In general, you can assume all instances in the same instancer will be rendered in a single draw call.
* Backends are free to optimize the ordering of draw calls to a certain extent, but utilities are provided to let
- * you control the order of draw calls
+ * you exert control over the ordering.
* Mesh Order
- *
For one, Meshes within a Model are guaranteed to render in the order they appear in their containing list.
+ * For one, Meshes within a Model are guaranteed to render in the order they appear in their containing list.
* This lets you e.g. preserve (or break!) vanilla's chunk RenderType order guarantees or control which Meshes of
- * your Model render over others.
+ * your Model render over others.
* Bias Order
- *
The other method is via the {@code bias} parameter to this method. An instancer with a lower bias will have
+ * The other method is via the {@code bias} parameter to this method. An instancer with a lower bias will have
* its instances draw BEFORE an instancer with a higher bias. This allows you to control the render order between
* your instances to e.g. create an "overlay" instance to selectively color or apply decals to another instance.
*
diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/BackendConfig.java b/common/src/backend/java/dev/engine_room/flywheel/backend/BackendConfig.java
new file mode 100644
index 000000000..1bc137773
--- /dev/null
+++ b/common/src/backend/java/dev/engine_room/flywheel/backend/BackendConfig.java
@@ -0,0 +1,17 @@
+package dev.engine_room.flywheel.backend;
+
+import dev.engine_room.flywheel.backend.compile.LightSmoothness;
+
+public interface BackendConfig {
+ BackendConfig INSTANCE = FlwBackendXplat.INSTANCE.getConfig();
+
+ /**
+ * How smooth/accurate our flw_light impl is.
+ *
+ * This makes more sense here as a backend-specific config because it's tightly coupled to
+ * our backend's implementation. 3rd party backend may have different approaches and configurations.
+ *
+ * @return The current light smoothness setting.
+ */
+ LightSmoothness lightSmoothness();
+}
diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/FlwBackendXplat.java b/common/src/backend/java/dev/engine_room/flywheel/backend/FlwBackendXplat.java
index 6acb2832a..8cd216c59 100644
--- a/common/src/backend/java/dev/engine_room/flywheel/backend/FlwBackendXplat.java
+++ b/common/src/backend/java/dev/engine_room/flywheel/backend/FlwBackendXplat.java
@@ -9,4 +9,6 @@ public interface FlwBackendXplat {
FlwBackendXplat INSTANCE = DependencyInjection.load(FlwBackendXplat.class, "dev.engine_room.flywheel.backend.FlwBackendXplatImpl");
int getLightEmission(BlockState state, BlockGetter level, BlockPos pos);
+
+ BackendConfig getConfig();
}
diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/LightSmoothnessArgument.java b/common/src/backend/java/dev/engine_room/flywheel/backend/LightSmoothnessArgument.java
new file mode 100644
index 000000000..871184d5d
--- /dev/null
+++ b/common/src/backend/java/dev/engine_room/flywheel/backend/LightSmoothnessArgument.java
@@ -0,0 +1,14 @@
+package dev.engine_room.flywheel.backend;
+
+import dev.engine_room.flywheel.backend.compile.LightSmoothness;
+import net.minecraft.commands.arguments.StringRepresentableArgument;
+import net.minecraft.commands.synchronization.SingletonArgumentInfo;
+
+public class LightSmoothnessArgument extends StringRepresentableArgument {
+ public static final LightSmoothnessArgument INSTANCE = new LightSmoothnessArgument();
+ public static final SingletonArgumentInfo INFO = SingletonArgumentInfo.contextFree(() -> INSTANCE);
+
+ public LightSmoothnessArgument() {
+ super(LightSmoothness.CODEC, LightSmoothness::values);
+ }
+}
diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/FlwPrograms.java b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/FlwPrograms.java
index 9eb89b41c..6681d4676 100644
--- a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/FlwPrograms.java
+++ b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/FlwPrograms.java
@@ -120,6 +120,7 @@ public final class FlwPrograms {
.build(loader);
}
+ // TODO: Do not uber this component. Shader compile times are very high now
@Nullable
private static UberShaderComponent createLightComponent(SourceLoader loader) {
return UberShaderComponent.builder(Flywheel.rl("light"))
diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/LightSmoothness.java b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/LightSmoothness.java
new file mode 100644
index 000000000..4ff0b8459
--- /dev/null
+++ b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/LightSmoothness.java
@@ -0,0 +1,38 @@
+package dev.engine_room.flywheel.backend.compile;
+
+import java.util.Locale;
+
+import com.mojang.serialization.Codec;
+
+import dev.engine_room.flywheel.backend.compile.core.Compilation;
+import net.minecraft.util.StringRepresentable;
+
+public enum LightSmoothness implements StringRepresentable {
+ FLAT(0, false),
+ TRI_LINEAR(1, false),
+ SMOOTH(2, false),
+ SMOOTH_INNER_FACE_CORRECTED(2, true),
+ ;
+
+ public static final Codec CODEC = StringRepresentable.fromEnum(LightSmoothness::values);
+
+ private final int smoothnessDefine;
+ private final boolean innerFaceCorrection;
+
+ LightSmoothness(int smoothnessDefine, boolean innerFaceCorrection) {
+ this.smoothnessDefine = smoothnessDefine;
+ this.innerFaceCorrection = innerFaceCorrection;
+ }
+
+ public void onCompile(Compilation comp) {
+ comp.define("_FLW_LIGHT_SMOOTHNESS", Integer.toString(smoothnessDefine));
+ if (innerFaceCorrection) {
+ comp.define("_FLW_INNER_FACE_CORRECTION");
+ }
+ }
+
+ @Override
+ public String getSerializedName() {
+ return name().toLowerCase(Locale.ROOT);
+ }
+}
diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/PipelineCompiler.java b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/PipelineCompiler.java
index a8d1652cf..bc303f8f9 100644
--- a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/PipelineCompiler.java
+++ b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/PipelineCompiler.java
@@ -4,6 +4,7 @@ import java.util.Collection;
import java.util.List;
import dev.engine_room.flywheel.api.Flywheel;
+import dev.engine_room.flywheel.backend.BackendConfig;
import dev.engine_room.flywheel.backend.InternalVertex;
import dev.engine_room.flywheel.backend.Samplers;
import dev.engine_room.flywheel.backend.compile.component.InstanceStructComponent;
@@ -25,6 +26,9 @@ public final class PipelineCompiler {
private static final ResourceLocation API_IMPL_FRAG = Flywheel.rl("internal/api_impl.frag");
static CompilationHarness create(ShaderSources sources, Pipeline pipeline, List vertexComponents, List fragmentComponents, Collection extensions) {
+ // We could technically compile every version of light smoothness ahead of time,
+ // but that seems unnecessary as I doubt most folks will be changing this option often.
+ var lightSmoothness = BackendConfig.INSTANCE.lightSmoothness();
return PIPELINE.program()
.link(PIPELINE.shader(GlCompat.MAX_GLSL_VERSION, ShaderType.VERTEX)
.nameMapper(key -> {
@@ -38,6 +42,7 @@ public final class PipelineCompiler {
.requireExtensions(extensions)
.onCompile((key, comp) -> key.contextShader()
.onCompile(comp))
+ .onCompile((key, comp) -> lightSmoothness.onCompile(comp))
.withResource(API_IMPL_VERT)
.withComponent(key -> new InstanceStructComponent(key.instanceType()))
.withResource(key -> key.instanceType()
@@ -57,6 +62,7 @@ public final class PipelineCompiler {
.enableExtension("GL_ARB_conservative_depth")
.onCompile((key, comp) -> key.contextShader()
.onCompile(comp))
+ .onCompile((key, comp) -> lightSmoothness.onCompile(comp))
.withResource(API_IMPL_FRAG)
.withComponents(fragmentComponents)
.withResource(pipeline.fragmentMain()))
diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.glsl b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.glsl
index 875a01ffd..1738c38e6 100644
--- a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.glsl
+++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.glsl
@@ -1,12 +1,11 @@
-// TODO: Add config for light smoothness. Should work at a compile flag level
+struct FlwLightAo {
+ vec2 light;
+ float ao;
+};
/// Get the light at the given world position relative to flw_renderOrigin from the given normal.
/// This may be interpolated for smooth lighting.
-bool flw_light(vec3 worldPos, vec3 normal, out vec2 light);
-
-/// Get the light at the given world position relative to flw_renderOrigin.
-/// This may be interpolated for smooth lighting.
-bool flw_light(vec3 worldPos, out vec2 light);
+bool flw_light(vec3 worldPos, vec3 normal, out FlwLightAo light);
/// Fetches the light value at the given block position.
/// Returns false if the light for the given block is not available.
diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/common.frag b/common/src/backend/resources/assets/flywheel/flywheel/internal/common.frag
index 52b013a52..aac8334b3 100644
--- a/common/src/backend/resources/assets/flywheel/flywheel/internal/common.frag
+++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/common.frag
@@ -45,6 +45,8 @@ void _flw_main() {
flw_fragColor.a *= crumblingSampleColor.a;
#endif
+ flw_shaderLight();
+
vec4 color = flw_fragColor;
if (flw_discardPredicate(color)) {
@@ -61,8 +63,6 @@ void _flw_main() {
vec4 lightColor = vec4(1.);
if (flw_material.useLight) {
- flw_shaderLight();
-
lightColor = texture(flw_lightTex, clamp(flw_fragLight, 0.5 / 16.0, 15.5 / 16.0));
color *= lightColor;
}
diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/light_lut.glsl b/common/src/backend/resources/assets/flywheel/flywheel/internal/light_lut.glsl
index 31c8e7594..d537f4515 100644
--- a/common/src/backend/resources/assets/flywheel/flywheel/internal/light_lut.glsl
+++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/light_lut.glsl
@@ -15,6 +15,10 @@ uint _flw_indexLut(uint index);
uint _flw_indexLight(uint index);
+// Adding this option takes my test world from ~800 to ~1250 FPS on my 3060ti.
+// I have not taken it to a profiler otherwise.
+#pragma optionNV (unroll all)
+
/// Find the index for the next step in the LUT.
/// @param base The base index in the LUT, should point to the start of a coordinate span.
/// @param coord The coordinate to look for.
@@ -77,8 +81,8 @@ uvec2 _flw_lightAt(uint sectionOffset, uvec3 blockInSectionPos) {
bool _flw_isSolid(uint sectionOffset, uvec3 blockInSectionPos) {
uint bitOffset = blockInSectionPos.x + blockInSectionPos.z * 18u + blockInSectionPos.y * 18u * 18u;
- uint uintOffset = bitOffset / 32u;
- uint bitInWordOffset = bitOffset % 32u;
+ uint uintOffset = bitOffset >> 5u;
+ uint bitInWordOffset = bitOffset & 31u;
uint word = _flw_indexLight(sectionOffset + _FLW_SOLID_START_INTS + uintOffset);
@@ -99,61 +103,14 @@ bool flw_lightFetch(ivec3 blockPos, out vec2 lightCoord) {
return true;
}
-bool flw_light(vec3 worldPos, out vec2 lightCoord) {
- // Always use the section of the block we are contained in to ensure accuracy.
- // We don't want to interpolate between sections, but also we might not be able
- // to rely on the existence neighboring sections, so don't do any extra rounding here.
- ivec3 blockPos = ivec3(floor(worldPos)) + flw_renderOrigin;
-
- uint lightSectionIndex;
- if (_flw_chunkCoordToSectionIndex(blockPos >> 4, lightSectionIndex)) {
- return false;
- }
- // The offset of the section in the light buffer.
- uint sectionOffset = lightSectionIndex * _FLW_LIGHT_SECTION_SIZE_INTS;
-
- // The block's position in the section adjusted into 18x18x18 space
- uvec3 blockInSectionPos = (blockPos & 0xF) + 1;
-
- // The lowest corner of the 2x2x2 area we'll be trilinear interpolating.
- // The ugly bit on the end evaluates to -1 or 0 depending on which side of 0.5 we are.
- uvec3 lowestCorner = blockInSectionPos + ivec3(floor(fract(worldPos) - 0.5));
-
- // The distance our fragment is from the center of the lowest corner.
- vec3 interpolant = fract(worldPos - 0.5);
-
- // Fetch everything for trilinear interpolation
- // Hypothetically we could re-order these and do some calculations in-between fetches
- // to help with latency hiding, but the compiler should be able to do that for us.
- vec2 light000 = vec2(_flw_lightAt(sectionOffset, lowestCorner));
- vec2 light001 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 0, 1)));
- vec2 light010 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 1, 0)));
- vec2 light011 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 1, 1)));
- vec2 light100 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 0, 0)));
- vec2 light101 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 0, 1)));
- vec2 light110 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 1, 0)));
- vec2 light111 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 1, 1)));
-
- vec2 light00 = mix(light000, light001, interpolant.z);
- vec2 light01 = mix(light010, light011, interpolant.z);
- vec2 light10 = mix(light100, light101, interpolant.z);
- vec2 light11 = mix(light110, light111, interpolant.z);
-
- vec2 light0 = mix(light00, light01, interpolant.y);
- vec2 light1 = mix(light10, light11, interpolant.y);
-
- lightCoord = mix(light0, light1, interpolant.x) / 15.;
- return true;
-}
-
-uint _flw_lightIndex(in uvec3 p) {
- return p.x + p.z * 3u + p.y * 9u;
-}
-
/// Premtively collect all light in a 3x3x3 area centered on our block.
/// Depending on the normal, we won't use all the data, but fetching on demand will have many duplicated fetches.
-uvec3[27] _flw_fetchLight3x3x3(uint sectionOffset, ivec3 blockInSectionPos, uint solid) {
- uvec3[27] lights;
+///
+/// The output is a 3-component vector packed into a single uint to save
+/// memory and ALU ops later on. 10 bits are used for each component. This allows 4 such packed ints to be added
+/// together with room to spare before overflowing into the next component.
+uint[27] _flw_fetchLight3x3x3(uint sectionOffset, ivec3 blockInSectionPos, uint solid) {
+ uint[27] lights;
uint index = 0u;
uint mask = 1u;
@@ -161,8 +118,13 @@ uvec3[27] _flw_fetchLight3x3x3(uint sectionOffset, ivec3 blockInSectionPos, uint
for (int z = -1; z <= 1; z++) {
for (int x = -1; x <= 1; x++) {
// 0 if the block is solid, 1 if it's not.
- uint flag = uint((solid & mask) == 0u);
- lights[index] = uvec3(_flw_lightAt(sectionOffset, uvec3(blockInSectionPos + ivec3(x, y, z))), flag);
+ uint notSolid = uint((solid & mask) == 0u);
+ uvec2 light = _flw_lightAt(sectionOffset, uvec3(blockInSectionPos + ivec3(x, y, z)));
+
+ lights[index] = light.x;
+ lights[index] |= (light.y) << 10;
+ lights[index] |= (notSolid) << 20;
+
index++;
mask <<= 1;
}
@@ -190,6 +152,10 @@ uint _flw_fetchSolid3x3x3(uint sectionOffset, ivec3 blockInSectionPos) {
return ret;
}
+#define _flw_index3x3x3(x, y, z) ((x) + (z) * 3u + (y) * 9u)
+#define _flw_index3x3x3v(p) _flw_index3x3x3((p).x, (p).y, (p).z)
+#define _flw_validCountToAo(validCount) (1. - (4. - (validCount)) * 0.2)
+
/// Calculate the light for a direction by averaging the light at the corners of the block.
///
/// To make this reusable across directions, c00..c11 choose what values relative to each corner to use.
@@ -200,39 +166,76 @@ uint _flw_fetchSolid3x3x3(uint sectionOffset, ivec3 blockInSectionPos) {
/// @param lights The light data for the 3x3x3 area.
/// @param interpolant The position within the center block.
/// @param c00..c11 4 offsets to determine which "direction" we are averaging.
-vec2 _flw_lightForDirection(in uvec3[27] lights, in vec3 interpolant, in uvec3 c00, in uvec3 c01, in uvec3 c10, in uvec3 c11) {
+/// @param oppositeMask A bitmask telling this function which bit to flip to get the opposite index for a given corner
+vec3 _flw_lightForDirection(uint[27] lights, vec3 interpolant, uvec3 c00, uvec3 c01, uvec3 c10, uvec3 c11, uint oppositeMask) {
+ // Constant propatation should inline all of these index calculations,
+ // but since they're distributive we can lay them out more nicely.
+ uint ic00 = _flw_index3x3x3v(c00);
+ uint ic01 = _flw_index3x3x3v(c01);
+ uint ic10 = _flw_index3x3x3v(c10);
+ uint ic11 = _flw_index3x3x3v(c11);
- uvec3 i000 = lights[_flw_lightIndex(c00 + uvec3(0u, 0u, 0u))] + lights[_flw_lightIndex(c01 + uvec3(0u, 0u, 0u))] + lights[_flw_lightIndex(c10 + uvec3(0u, 0u, 0u))] + lights[_flw_lightIndex(c11 + uvec3(0u, 0u, 0u))];
- uvec3 i001 = lights[_flw_lightIndex(c00 + uvec3(0u, 0u, 1u))] + lights[_flw_lightIndex(c01 + uvec3(0u, 0u, 1u))] + lights[_flw_lightIndex(c10 + uvec3(0u, 0u, 1u))] + lights[_flw_lightIndex(c11 + uvec3(0u, 0u, 1u))];
- uvec3 i010 = lights[_flw_lightIndex(c00 + uvec3(0u, 1u, 0u))] + lights[_flw_lightIndex(c01 + uvec3(0u, 1u, 0u))] + lights[_flw_lightIndex(c10 + uvec3(0u, 1u, 0u))] + lights[_flw_lightIndex(c11 + uvec3(0u, 1u, 0u))];
- uvec3 i011 = lights[_flw_lightIndex(c00 + uvec3(0u, 1u, 1u))] + lights[_flw_lightIndex(c01 + uvec3(0u, 1u, 1u))] + lights[_flw_lightIndex(c10 + uvec3(0u, 1u, 1u))] + lights[_flw_lightIndex(c11 + uvec3(0u, 1u, 1u))];
- uvec3 i100 = lights[_flw_lightIndex(c00 + uvec3(1u, 0u, 0u))] + lights[_flw_lightIndex(c01 + uvec3(1u, 0u, 0u))] + lights[_flw_lightIndex(c10 + uvec3(1u, 0u, 0u))] + lights[_flw_lightIndex(c11 + uvec3(1u, 0u, 0u))];
- uvec3 i101 = lights[_flw_lightIndex(c00 + uvec3(1u, 0u, 1u))] + lights[_flw_lightIndex(c01 + uvec3(1u, 0u, 1u))] + lights[_flw_lightIndex(c10 + uvec3(1u, 0u, 1u))] + lights[_flw_lightIndex(c11 + uvec3(1u, 0u, 1u))];
- uvec3 i110 = lights[_flw_lightIndex(c00 + uvec3(1u, 1u, 0u))] + lights[_flw_lightIndex(c01 + uvec3(1u, 1u, 0u))] + lights[_flw_lightIndex(c10 + uvec3(1u, 1u, 0u))] + lights[_flw_lightIndex(c11 + uvec3(1u, 1u, 0u))];
- uvec3 i111 = lights[_flw_lightIndex(c00 + uvec3(1u, 1u, 1u))] + lights[_flw_lightIndex(c01 + uvec3(1u, 1u, 1u))] + lights[_flw_lightIndex(c10 + uvec3(1u, 1u, 1u))] + lights[_flw_lightIndex(c11 + uvec3(1u, 1u, 1u))];
+ const uint[8] corners = uint[](
+ _flw_index3x3x3(0u, 0u, 0u),
+ _flw_index3x3x3(0u, 0u, 1u),
+ _flw_index3x3x3(0u, 1u, 0u),
+ _flw_index3x3x3(0u, 1u, 1u),
+ _flw_index3x3x3(1u, 0u, 0u),
+ _flw_index3x3x3(1u, 0u, 1u),
+ _flw_index3x3x3(1u, 1u, 0u),
+ _flw_index3x3x3(1u, 1u, 1u)
+ );
- // Divide by the number of light transmitting blocks to get the average.
- vec2 light000 = i000.z == 0 ? vec2(0) : vec2(i000.xy) / float(i000.z);
- vec2 light001 = i001.z == 0 ? vec2(0) : vec2(i001.xy) / float(i001.z);
- vec2 light010 = i010.z == 0 ? vec2(0) : vec2(i010.xy) / float(i010.z);
- vec2 light011 = i011.z == 0 ? vec2(0) : vec2(i011.xy) / float(i011.z);
- vec2 light100 = i100.z == 0 ? vec2(0) : vec2(i100.xy) / float(i100.z);
- vec2 light101 = i101.z == 0 ? vec2(0) : vec2(i101.xy) / float(i101.z);
- vec2 light110 = i110.z == 0 ? vec2(0) : vec2(i110.xy) / float(i110.z);
- vec2 light111 = i111.z == 0 ? vec2(0) : vec2(i111.xy) / float(i111.z);
+ // Division and branching are both kinda expensive, so use this table for the valid block normalization
+ const float[5] normalizers = float[](0., 1., 1. / 2., 1. / 3., 1. / 4.);
- vec2 light00 = mix(light000, light001, interpolant.z);
- vec2 light01 = mix(light010, light011, interpolant.z);
- vec2 light10 = mix(light100, light101, interpolant.z);
- vec2 light11 = mix(light110, light111, interpolant.z);
+ // Sum up the light and number of valid blocks in each corner for this direction
+ uint[8] summed;
+ for (uint i = 0; i < 8; i++) {
+ uint corner = corners[i];
+ summed[i] = lights[ic00 + corner] + lights[ic01 + corner] + lights[ic10 + corner] + lights[ic11 + corner];
+ }
- vec2 light0 = mix(light00, light01, interpolant.y);
- vec2 light1 = mix(light10, light11, interpolant.y);
+ // The final light and number of valid blocks for each corner.
+ vec3[8] adjusted;
+ for (uint i = 0; i < 8; i++) {
+ #ifdef _FLW_INNER_FACE_CORRECTION
+ // If the current corner has no valid blocks, use the opposite
+ // corner's light based on which direction we're evaluating.
+ // Because of how our corners are indexed, moving along one axis is the same as flipping a bit.
+ uint cornerIndex = (summed[i] & 0xFFF00000u) == 0u ? i ^ oppositeMask : i;
+ #else
+ uint cornerIndex = i;
+ #endif
+ uint corner = summed[cornerIndex];
- return mix(light0, light1, interpolant.x) / 15.;
+ uvec3 unpacked = uvec3(corner & 0x3FFu, (corner >> 10u) & 0x3FFu, corner >> 20u);
+
+ // Normalize by the number of valid blocks.
+ adjusted[i].xy = vec2(unpacked.xy) * normalizers[unpacked.z];
+ adjusted[i].z = float(unpacked.z);
+ }
+
+ // Trilinear interpolation, including valid count
+ vec3 light00 = mix(adjusted[0], adjusted[1], interpolant.z);
+ vec3 light01 = mix(adjusted[2], adjusted[3], interpolant.z);
+ vec3 light10 = mix(adjusted[4], adjusted[5], interpolant.z);
+ vec3 light11 = mix(adjusted[6], adjusted[7], interpolant.z);
+
+ vec3 light0 = mix(light00, light01, interpolant.y);
+ vec3 light1 = mix(light10, light11, interpolant.y);
+
+ vec3 light = mix(light0, light1, interpolant.x);
+
+ // Normalize the light coords
+ light.xy *= 1. / 15.;
+ // Calculate the AO multiplier from the number of valid blocks
+ light.z = _flw_validCountToAo(light.z);
+
+ return light;
}
-bool flw_light(vec3 worldPos, vec3 normal, out vec2 lightCoord) {
+bool flw_light(vec3 worldPos, vec3 normal, out FlwLightAo light) {
// Always use the section of the block we are contained in to ensure accuracy.
// We don't want to interpolate between sections, but also we might not be able
// to rely on the existence neighboring sections, so don't do any extra rounding here.
@@ -248,48 +251,96 @@ bool flw_light(vec3 worldPos, vec3 normal, out vec2 lightCoord) {
// The block's position in the section adjusted into 18x18x18 space
ivec3 blockInSectionPos = (blockPos & 0xF) + 1;
+ #if _FLW_LIGHT_SMOOTHNESS == 1// Directly trilerp as if sampling a texture
+
+ // The lowest corner of the 2x2x2 area we'll be trilinear interpolating.
+ // The ugly bit on the end evaluates to -1 or 0 depending on which side of 0.5 we are.
+ uvec3 lowestCorner = blockInSectionPos + ivec3(floor(fract(worldPos) - 0.5));
+
+ // The distance our fragment is from the center of the lowest corner.
+ vec3 interpolant = fract(worldPos - 0.5);
+
+ // Fetch everything for trilinear interpolation
+ // Hypothetically we could re-order these and do some calculations in-between fetches
+ // to help with latency hiding, but the compiler should be able to do that for us.
+ vec2 light000 = vec2(_flw_lightAt(sectionOffset, lowestCorner));
+ vec2 light100 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 0, 0)));
+ vec2 light001 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 0, 1)));
+ vec2 light101 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 0, 1)));
+ vec2 light010 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 1, 0)));
+ vec2 light110 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 1, 0)));
+ vec2 light011 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 1, 1)));
+ vec2 light111 = vec2(_flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 1, 1)));
+
+ vec2 light00 = mix(light000, light001, interpolant.z);
+ vec2 light01 = mix(light010, light011, interpolant.z);
+ vec2 light10 = mix(light100, light101, interpolant.z);
+ vec2 light11 = mix(light110, light111, interpolant.z);
+
+ vec2 light0 = mix(light00, light01, interpolant.y);
+ vec2 light1 = mix(light10, light11, interpolant.y);
+
+ light.light = mix(light0, light1, interpolant.x) / 15.;
+ light.ao = 1.;
+
+ #elif _FLW_LIGHT_SMOOTHNESS == 2// Lighting and AO accurate to chunk baking
+
uint solid = _flw_fetchSolid3x3x3(sectionOffset, blockInSectionPos);
if (solid == _FLW_COMPLETELY_SOLID) {
- lightCoord = vec2(0.);
+ // No point in doing any work if the entire 3x3x3 volume around us is filled.
+ // Kinda rare but this may happen if our fragment is in the middle of a lot of tinted glass
+ light.light = vec2(0.);
+ light.ao = _flw_validCountToAo(0.);
return true;
}
// Fetch everything in a 3x3x3 area centered around the block.
- uvec3[27] lights = _flw_fetchLight3x3x3(sectionOffset, blockInSectionPos, solid);
+ uint[27] lights = _flw_fetchLight3x3x3(sectionOffset, blockInSectionPos, solid);
vec3 interpolant = fract(worldPos);
- vec2 lightX;
+ // Average the light in relevant directions at each corner, skipping directions that would have no influence
+
+ vec3 lightX;
if (normal.x > _FLW_EPSILON) {
- lightX = _flw_lightForDirection(lights, interpolant, uvec3(1u, 0u, 0u), uvec3(1u, 0u, 1u), uvec3(1u, 1u, 0u), uvec3(1u, 1u, 1u));
+ lightX = _flw_lightForDirection(lights, interpolant, uvec3(1u, 0u, 0u), uvec3(1u, 0u, 1u), uvec3(1u, 1u, 0u), uvec3(1u, 1u, 1u), 4u);
} else if (normal.x < -_FLW_EPSILON) {
- lightX = _flw_lightForDirection(lights, interpolant, uvec3(0u, 0u, 0u), uvec3(0u, 0u, 1u), uvec3(0u, 1u, 0u), uvec3(0u, 1u, 1u));
+ lightX = _flw_lightForDirection(lights, interpolant, uvec3(0u, 0u, 0u), uvec3(0u, 0u, 1u), uvec3(0u, 1u, 0u), uvec3(0u, 1u, 1u), 4u);
} else {
- lightX = vec2(0.);
+ lightX = vec3(0.);
}
- vec2 lightZ;
+ vec3 lightZ;
if (normal.z > _FLW_EPSILON) {
- lightZ = _flw_lightForDirection(lights, interpolant, uvec3(0u, 0u, 1u), uvec3(0u, 1u, 1u), uvec3(1u, 0u, 1u), uvec3(1u, 1u, 1u));
+ lightZ = _flw_lightForDirection(lights, interpolant, uvec3(0u, 0u, 1u), uvec3(0u, 1u, 1u), uvec3(1u, 0u, 1u), uvec3(1u, 1u, 1u), 1u);
} else if (normal.z < -_FLW_EPSILON) {
- lightZ = _flw_lightForDirection(lights, interpolant, uvec3(0u, 0u, 0u), uvec3(0u, 1u, 0u), uvec3(1u, 0u, 0u), uvec3(1u, 1u, 0u));
+ lightZ = _flw_lightForDirection(lights, interpolant, uvec3(0u, 0u, 0u), uvec3(0u, 1u, 0u), uvec3(1u, 0u, 0u), uvec3(1u, 1u, 0u), 1u);
} else {
- lightZ = vec2(0.);
+ lightZ = vec3(0.);
}
- vec2 lightY;
- // Average the light in relevant directions at each corner.
+ vec3 lightY;
if (normal.y > _FLW_EPSILON) {
- lightY = _flw_lightForDirection(lights, interpolant, uvec3(0u, 1u, 0u), uvec3(0u, 1u, 1u), uvec3(1u, 1u, 0u), uvec3(1u, 1u, 1u));
+ lightY = _flw_lightForDirection(lights, interpolant, uvec3(0u, 1u, 0u), uvec3(0u, 1u, 1u), uvec3(1u, 1u, 0u), uvec3(1u, 1u, 1u), 2u);
} else if (normal.y < -_FLW_EPSILON) {
- lightY = _flw_lightForDirection(lights, interpolant, uvec3(0u, 0u, 0u), uvec3(0u, 0u, 1u), uvec3(1u, 0u, 0u), uvec3(1u, 0u, 1u));
+ lightY = _flw_lightForDirection(lights, interpolant, uvec3(0u, 0u, 0u), uvec3(0u, 0u, 1u), uvec3(1u, 0u, 0u), uvec3(1u, 0u, 1u), 2u);
} else {
- lightY = vec2(0.);
+ lightY = vec3(0.);
}
vec3 n2 = normal * normal;
- lightCoord = lightX * n2.x + lightY * n2.y + lightZ * n2.z;
+ vec3 lightAo = lightX * n2.x + lightY * n2.y + lightZ * n2.z;
+
+ light.light = lightAo.xy;
+ light.ao = lightAo.z;
+
+ #else// Entirely flat lighting, the lowest setting and a fallback in case an invalid option is set
+
+ light.light = vec2(_flw_lightAt(sectionOffset, blockInSectionPos)) / 15.;
+ light.ao = 1.;
+
+ #endif
return true;
}
diff --git a/common/src/lib/resources/assets/flywheel/flywheel/light/smooth.glsl b/common/src/lib/resources/assets/flywheel/flywheel/light/smooth.glsl
index 6d6d83ef2..4844308d9 100644
--- a/common/src/lib/resources/assets/flywheel/flywheel/light/smooth.glsl
+++ b/common/src/lib/resources/assets/flywheel/flywheel/light/smooth.glsl
@@ -1,6 +1,8 @@
void flw_shaderLight() {
- vec2 embeddedLight;
- if (flw_light(flw_vertexPos.xyz, flw_vertexNormal, embeddedLight)) {
- flw_fragLight = max(flw_fragLight, embeddedLight);
+ FlwLightAo light;
+ if (flw_light(flw_vertexPos.xyz, flw_vertexNormal, light)) {
+ flw_fragLight = max(flw_fragLight, light.light);
+
+ flw_fragColor.rgb *= light.ao;
}
}
diff --git a/common/src/lib/resources/assets/flywheel/flywheel/light/smooth_when_embedded.glsl b/common/src/lib/resources/assets/flywheel/flywheel/light/smooth_when_embedded.glsl
index d8dc6fd51..035c6e9ad 100644
--- a/common/src/lib/resources/assets/flywheel/flywheel/light/smooth_when_embedded.glsl
+++ b/common/src/lib/resources/assets/flywheel/flywheel/light/smooth_when_embedded.glsl
@@ -1,8 +1,10 @@
void flw_shaderLight() {
#ifdef FLW_EMBEDDED
- vec2 embeddedLight;
- if (flw_light(flw_vertexPos.xyz, flw_vertexNormal, embeddedLight)) {
- flw_fragLight = max(flw_fragLight, embeddedLight);
+ FlwLightAo light;
+ if (flw_light(flw_vertexPos.xyz, flw_vertexNormal, light)) {
+ flw_fragLight = max(flw_fragLight, light.light);
+
+ flw_fragColor.rgb *= light.ao;
}
#endif
}
diff --git a/common/src/main/java/dev/engine_room/flywheel/vanilla/ShulkerBoxVisual.java b/common/src/main/java/dev/engine_room/flywheel/vanilla/ShulkerBoxVisual.java
index 9e9e17925..716b8a317 100644
--- a/common/src/main/java/dev/engine_room/flywheel/vanilla/ShulkerBoxVisual.java
+++ b/common/src/main/java/dev/engine_room/flywheel/vanilla/ShulkerBoxVisual.java
@@ -8,10 +8,12 @@ import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import dev.engine_room.flywheel.api.instance.Instance;
+import dev.engine_room.flywheel.api.visual.ShaderLightVisual;
import dev.engine_room.flywheel.api.visualization.VisualizationContext;
import dev.engine_room.flywheel.lib.instance.InstanceTypes;
import dev.engine_room.flywheel.lib.instance.TransformedInstance;
import dev.engine_room.flywheel.lib.material.CutoutShaders;
+import dev.engine_room.flywheel.lib.material.LightShaders;
import dev.engine_room.flywheel.lib.material.SimpleMaterial;
import dev.engine_room.flywheel.lib.model.ModelCache;
import dev.engine_room.flywheel.lib.model.SingleMeshModel;
@@ -19,17 +21,20 @@ import dev.engine_room.flywheel.lib.model.part.ModelPartConverter;
import dev.engine_room.flywheel.lib.transform.TransformStack;
import dev.engine_room.flywheel.lib.visual.AbstractBlockEntityVisual;
import dev.engine_room.flywheel.lib.visual.SimpleDynamicVisual;
+import it.unimi.dsi.fastutil.longs.LongArraySet;
import net.minecraft.client.model.geom.ModelLayers;
import net.minecraft.client.renderer.Sheets;
import net.minecraft.client.resources.model.Material;
import net.minecraft.core.Direction;
+import net.minecraft.core.SectionPos;
import net.minecraft.world.item.DyeColor;
import net.minecraft.world.level.block.ShulkerBoxBlock;
import net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity;
-public class ShulkerBoxVisual extends AbstractBlockEntityVisual implements SimpleDynamicVisual {
+public class ShulkerBoxVisual extends AbstractBlockEntityVisual implements SimpleDynamicVisual, ShaderLightVisual {
private static final dev.engine_room.flywheel.api.material.Material MATERIAL = SimpleMaterial.builder()
.cutout(CutoutShaders.ONE_TENTH)
+ .light(LightShaders.SMOOTH)
.texture(Sheets.SHULKER_SHEET)
.mipmap(false)
.backfaceCulling(false)
@@ -67,6 +72,7 @@ public class ShulkerBoxVisual extends AbstractBlockEntityVisual {
+ var oldValue = FabricBackendConfig.INSTANCE.lightSmoothness;
+ var newValue = context.getArgument("mode", LightSmoothness.class);
+
+ if (oldValue != newValue) {
+ FabricBackendConfig.INSTANCE.lightSmoothness = newValue;
+ FabricBackendConfig.INSTANCE.save();
+ Minecraft.getInstance()
+ .reloadResourcePacks();
+ }
+ return Command.SINGLE_SUCCESS;
+ })));
+
dispatcher.register(command);
}
diff --git a/fabric/src/main/java/dev/engine_room/flywheel/impl/FlywheelFabric.java b/fabric/src/main/java/dev/engine_room/flywheel/impl/FlywheelFabric.java
index 26db5c4f7..5e8ab76ac 100644
--- a/fabric/src/main/java/dev/engine_room/flywheel/impl/FlywheelFabric.java
+++ b/fabric/src/main/java/dev/engine_room/flywheel/impl/FlywheelFabric.java
@@ -5,6 +5,7 @@ import org.jetbrains.annotations.UnknownNullability;
import dev.engine_room.flywheel.api.Flywheel;
import dev.engine_room.flywheel.api.event.EndClientResourceReloadCallback;
import dev.engine_room.flywheel.api.event.ReloadLevelRendererCallback;
+import dev.engine_room.flywheel.backend.LightSmoothnessArgument;
import dev.engine_room.flywheel.backend.compile.FlwProgramsReloader;
import dev.engine_room.flywheel.backend.engine.uniform.Uniforms;
import dev.engine_room.flywheel.impl.visualization.VisualizationEventHandler;
@@ -62,6 +63,7 @@ public final class FlywheelFabric implements ClientModInitializer {
ArgumentTypeRegistry.registerArgumentType(Flywheel.rl("backend"), BackendArgument.class, BackendArgument.INFO);
ArgumentTypeRegistry.registerArgumentType(Flywheel.rl("debug_mode"), DebugModeArgument.class, DebugModeArgument.INFO);
+ ArgumentTypeRegistry.registerArgumentType(Flywheel.rl("light_smoothness"), LightSmoothnessArgument.class, LightSmoothnessArgument.INFO);
}
private static void setupLib() {
diff --git a/fabric/src/main/java/dev/engine_room/flywheel/impl/mixin/MinecraftMixin.java b/fabric/src/main/java/dev/engine_room/flywheel/impl/mixin/MinecraftMixin.java
index dcab8eeec..765bfe3b0 100644
--- a/fabric/src/main/java/dev/engine_room/flywheel/impl/mixin/MinecraftMixin.java
+++ b/fabric/src/main/java/dev/engine_room/flywheel/impl/mixin/MinecraftMixin.java
@@ -12,6 +12,7 @@ import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.engine_room.flywheel.api.event.EndClientResourceReloadCallback;
+import dev.engine_room.flywheel.backend.FabricBackendConfig;
import dev.engine_room.flywheel.impl.FabricFlwConfig;
import dev.engine_room.flywheel.impl.FlwImpl;
import net.minecraft.client.Minecraft;
@@ -29,6 +30,7 @@ abstract class MinecraftMixin {
// Load the config after we freeze registries,
// so we can find third party backends.
FabricFlwConfig.INSTANCE.load();
+ FabricBackendConfig.INSTANCE.load();
}
@Inject(method = "method_53522", at = @At("HEAD"))
diff --git a/forge/src/backend/java/dev/engine_room/flywheel/backend/FlwBackendXplatImpl.java b/forge/src/backend/java/dev/engine_room/flywheel/backend/FlwBackendXplatImpl.java
index f49549f3e..6c6bd2c68 100644
--- a/forge/src/backend/java/dev/engine_room/flywheel/backend/FlwBackendXplatImpl.java
+++ b/forge/src/backend/java/dev/engine_room/flywheel/backend/FlwBackendXplatImpl.java
@@ -9,4 +9,9 @@ public class FlwBackendXplatImpl implements FlwBackendXplat {
public int getLightEmission(BlockState state, BlockGetter level, BlockPos pos) {
return state.getLightEmission(level, pos);
}
+
+ @Override
+ public BackendConfig getConfig() {
+ return ForgeBackendConfig.INSTANCE;
+ }
}
diff --git a/forge/src/backend/java/dev/engine_room/flywheel/backend/ForgeBackendConfig.java b/forge/src/backend/java/dev/engine_room/flywheel/backend/ForgeBackendConfig.java
new file mode 100644
index 000000000..3d3e64963
--- /dev/null
+++ b/forge/src/backend/java/dev/engine_room/flywheel/backend/ForgeBackendConfig.java
@@ -0,0 +1,39 @@
+package dev.engine_room.flywheel.backend;
+
+import org.apache.commons.lang3.tuple.Pair;
+
+import dev.engine_room.flywheel.backend.compile.LightSmoothness;
+import net.minecraftforge.common.ForgeConfigSpec;
+import net.minecraftforge.fml.ModLoadingContext;
+import net.minecraftforge.fml.config.ModConfig;
+
+public class ForgeBackendConfig implements BackendConfig {
+ public static final ForgeBackendConfig INSTANCE = new ForgeBackendConfig();
+
+ public final ClientConfig client;
+ private final ForgeConfigSpec clientSpec;
+
+ private ForgeBackendConfig() {
+ Pair clientPair = new ForgeConfigSpec.Builder().configure(ClientConfig::new);
+ this.client = clientPair.getLeft();
+ clientSpec = clientPair.getRight();
+ }
+
+ @Override
+ public LightSmoothness lightSmoothness() {
+ return client.lightSmoothness.get();
+ }
+
+ public void registerSpecs(ModLoadingContext context) {
+ context.registerConfig(ModConfig.Type.CLIENT, clientSpec, "flywheel-backend.toml");
+ }
+
+ public static class ClientConfig {
+ public final ForgeConfigSpec.EnumValue lightSmoothness;
+
+ private ClientConfig(ForgeConfigSpec.Builder builder) {
+ lightSmoothness = builder.comment("How smooth flywheel's shader-based lighting should be. May have a large performance impact.")
+ .defineEnum("lightSmoothness", LightSmoothness.SMOOTH);
+ }
+ }
+}
diff --git a/forge/src/main/java/dev/engine_room/flywheel/impl/FlwCommands.java b/forge/src/main/java/dev/engine_room/flywheel/impl/FlwCommands.java
index 37f4facf9..f02ce6543 100644
--- a/forge/src/main/java/dev/engine_room/flywheel/impl/FlwCommands.java
+++ b/forge/src/main/java/dev/engine_room/flywheel/impl/FlwCommands.java
@@ -6,6 +6,9 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import dev.engine_room.flywheel.api.backend.Backend;
import dev.engine_room.flywheel.api.backend.BackendManager;
+import dev.engine_room.flywheel.backend.ForgeBackendConfig;
+import dev.engine_room.flywheel.backend.LightSmoothnessArgument;
+import dev.engine_room.flywheel.backend.compile.LightSmoothness;
import dev.engine_room.flywheel.backend.engine.uniform.DebugMode;
import dev.engine_room.flywheel.backend.engine.uniform.FrameUniforms;
import net.minecraft.client.Minecraft;
@@ -121,6 +124,21 @@ public final class FlwCommands {
return Command.SINGLE_SUCCESS;
})));
+ var lightSmoothnessValue = ForgeBackendConfig.INSTANCE.client.lightSmoothness;
+ command.then(Commands.literal("lightSmoothness")
+ .then(Commands.argument("mode", LightSmoothnessArgument.INSTANCE)
+ .executes(context -> {
+ var oldValue = lightSmoothnessValue.get();
+ var newValue = context.getArgument("mode", LightSmoothness.class);
+
+ if (oldValue != newValue) {
+ lightSmoothnessValue.set(newValue);
+ Minecraft.getInstance()
+ .reloadResourcePacks();
+ }
+ return Command.SINGLE_SUCCESS;
+ })));
+
event.getDispatcher().register(command);
}
diff --git a/forge/src/main/java/dev/engine_room/flywheel/impl/FlywheelForge.java b/forge/src/main/java/dev/engine_room/flywheel/impl/FlywheelForge.java
index d8ca9ea23..6a6209151 100644
--- a/forge/src/main/java/dev/engine_room/flywheel/impl/FlywheelForge.java
+++ b/forge/src/main/java/dev/engine_room/flywheel/impl/FlywheelForge.java
@@ -8,6 +8,8 @@ import org.jetbrains.annotations.UnknownNullability;
import dev.engine_room.flywheel.api.Flywheel;
import dev.engine_room.flywheel.api.event.EndClientResourceReloadEvent;
import dev.engine_room.flywheel.api.event.ReloadLevelRendererEvent;
+import dev.engine_room.flywheel.backend.ForgeBackendConfig;
+import dev.engine_room.flywheel.backend.LightSmoothnessArgument;
import dev.engine_room.flywheel.backend.compile.FlwProgramsReloader;
import dev.engine_room.flywheel.backend.engine.uniform.Uniforms;
import dev.engine_room.flywheel.impl.visualization.VisualizationEventHandler;
@@ -49,6 +51,7 @@ public final class FlywheelForge {
IEventBus forgeEventBus = NeoForge.EVENT_BUS;
ForgeFlwConfig.INSTANCE.registerSpecs(modContainer);
+ ForgeBackendConfig.INSTANCE.registerSpecs(modLoadingContext);
if (FMLLoader.getDist().isClient()) {
Supplier toRun = () -> () -> FlywheelForge.clientInit(forgeEventBus, modEventBus);
@@ -94,11 +97,13 @@ public final class FlywheelForge {
modEventBus.addListener((FMLCommonSetupEvent e) -> {
ArgumentTypeInfos.registerByClass(BackendArgument.class, BackendArgument.INFO);
ArgumentTypeInfos.registerByClass(DebugModeArgument.class, DebugModeArgument.INFO);
+ ArgumentTypeInfos.registerByClass(LightSmoothnessArgument.class, LightSmoothnessArgument.INFO);
});
modEventBus.addListener((RegisterEvent e) -> {
if (e.getRegistryKey().equals(Registries.COMMAND_ARGUMENT_TYPE)) {
e.register(Registries.COMMAND_ARGUMENT_TYPE, Flywheel.rl("backend"), () -> BackendArgument.INFO);
e.register(Registries.COMMAND_ARGUMENT_TYPE, Flywheel.rl("debug_mode"), () -> DebugModeArgument.INFO);
+ e.register(ForgeRegistries.Keys.COMMAND_ARGUMENT_TYPES, Flywheel.rl("light_smoothness"), () -> LightSmoothnessArgument.INFO);
}
});
}