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); } }); }