Lighting, for instance

- Sideport light lut stuffs to instancing engine
- Move actual lookup logic to light_lut.glsl, and have backend mains
  provide functions to index the backing storages for sanity's sake
- Standardize naming of lut and sections
- Pull in pepper's loom fix, so I can build :lwe:
- Allow specifying the internal format of texture buffers so light can
  be a simple uint array
This commit is contained in:
Jozufozu 2024-07-05 17:40:55 -07:00
parent 253de1f343
commit 495c047968
13 changed files with 236 additions and 136 deletions

View file

@ -8,4 +8,6 @@ public class Samplers {
public static final GlTextureUnit LIGHT = GlTextureUnit.T2;
public static final GlTextureUnit CRUMBLING = GlTextureUnit.T3;
public static final GlTextureUnit INSTANCE_BUFFER = GlTextureUnit.T4;
public static final GlTextureUnit LIGHT_LUT = GlTextureUnit.T5;
public static final GlTextureUnit LIGHT_SECTIONS = GlTextureUnit.T6;
}

View file

@ -11,7 +11,11 @@ public final class Pipelines {
.vertexMain(Flywheel.rl("internal/instancing/main.vert"))
.fragmentMain(Flywheel.rl("internal/instancing/main.frag"))
.assembler(BufferTextureInstanceComponent::new)
.onLink(program -> program.setSamplerBinding("_flw_instances", Samplers.INSTANCE_BUFFER))
.onLink(program -> {
program.setSamplerBinding("_flw_instances", Samplers.INSTANCE_BUFFER);
program.setSamplerBinding("_flw_lightLut", Samplers.LIGHT_LUT);
program.setSamplerBinding("_flw_lightSections", Samplers.LIGHT_SECTIONS);
})
.build();
public static final Pipeline INDIRECT = Pipeline.builder()

View file

@ -8,6 +8,7 @@ import dev.engine_room.flywheel.api.event.RenderContext;
import dev.engine_room.flywheel.api.task.Plan;
import dev.engine_room.flywheel.backend.engine.EnvironmentStorage;
import dev.engine_room.flywheel.backend.engine.indirect.StagingBuffer;
import dev.engine_room.flywheel.backend.gl.buffer.GlBuffer;
import dev.engine_room.flywheel.lib.task.SimplePlan;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.Long2IntMap;
@ -205,10 +206,6 @@ public class LightStorage {
arena.delete();
}
public boolean hasChanges() {
return !changed.isEmpty();
}
public boolean checkNeedsLutRebuildAndClear() {
var out = needsLutRebuild;
needsLutRebuild = false;
@ -222,6 +219,15 @@ public class LightStorage {
changed.clear();
}
public void upload(GlBuffer buffer) {
if (changed.isEmpty()) {
return;
}
buffer.upload(arena.indexToPointer(0), arena.capacity() * SECTION_SIZE_BYTES);
changed.clear();
}
public IntArrayList createLut() {
// TODO: incremental lut updates
return LightLut.buildLut(section2ArenaIndex);

View file

@ -6,8 +6,8 @@ public class BufferBindings {
public static final int MODEL_INDEX_BUFFER_BINDING = 2;
public static final int MODEL_BUFFER_BINDING = 3;
public static final int DRAW_BUFFER_BINDING = 4;
public static final int EMBEDDING_LUT_BINDING = 5;
public static final int EMBEDDING_LIGHT_BINDING = 6;
public static final int LIGHT_LUT_BINDING = 5;
public static final int LIGHT_SECTION_BINDING = 6;
private BufferBindings() {
}

View file

@ -6,7 +6,7 @@ import org.lwjgl.system.MemoryUtil;
import dev.engine_room.flywheel.backend.engine.embed.LightStorage;
public class LightBuffers {
private final ResizableStorageArray lightArena = new ResizableStorageArray(LightStorage.SECTION_SIZE_BYTES);
private final ResizableStorageArray sections = new ResizableStorageArray(LightStorage.SECTION_SIZE_BYTES);
private final ResizableStorageArray lut = new ResizableStorageArray(4);
public LightBuffers() {
@ -19,8 +19,8 @@ public class LightBuffers {
return;
}
lightArena.ensureCapacity(capacity);
light.uploadChangedSections(staging, lightArena.handle());
sections.ensureCapacity(capacity);
light.uploadChangedSections(staging, sections.handle());
if (light.checkNeedsLutRebuildAndClear()) {
var lut = light.createLut();
@ -36,11 +36,11 @@ public class LightBuffers {
}
public void bind() {
if (lightArena.capacity() == 0) {
if (sections.capacity() == 0) {
return;
}
GL46.glBindBufferRange(GL46.GL_SHADER_STORAGE_BUFFER, BufferBindings.EMBEDDING_LUT_BINDING, lut.handle(), 0, lut.byteCapacity());
GL46.glBindBufferRange(GL46.GL_SHADER_STORAGE_BUFFER, BufferBindings.EMBEDDING_LIGHT_BINDING, lightArena.handle(), 0, lightArena.byteCapacity());
GL46.glBindBufferRange(GL46.GL_SHADER_STORAGE_BUFFER, BufferBindings.LIGHT_LUT_BINDING, lut.handle(), 0, lut.byteCapacity());
GL46.glBindBufferRange(GL46.GL_SHADER_STORAGE_BUFFER, BufferBindings.LIGHT_SECTION_BINDING, sections.handle(), 0, sections.byteCapacity());
}
}

View file

@ -43,6 +43,7 @@ public class InstancedDrawManager extends DrawManager<InstancedInstancer<?>> {
private final MeshPool meshPool;
private final GlVertexArray vao;
private final TextureBuffer instanceTexture;
private final InstancedLight light;
public InstancedDrawManager(InstancingPrograms programs) {
programs.acquire();
@ -51,6 +52,7 @@ public class InstancedDrawManager extends DrawManager<InstancedInstancer<?>> {
meshPool = new MeshPool();
vao = GlVertexArray.create();
instanceTexture = new TextureBuffer();
light = new InstancedLight();
meshPool.bind(vao);
}
@ -78,6 +80,8 @@ public class InstancedDrawManager extends DrawManager<InstancedInstancer<?>> {
}
meshPool.flush();
light.flush(lightStorage);
}
@Override
@ -92,6 +96,7 @@ public class InstancedDrawManager extends DrawManager<InstancedInstancer<?>> {
Uniforms.bindAll();
vao.bindForDraw();
TextureBinder.bindLightAndOverlay();
light.bind();
drawSet.draw(instanceTexture, programs);
@ -114,6 +119,8 @@ public class InstancedDrawManager extends DrawManager<InstancedInstancer<?>> {
programs.release();
vao.delete();
light.delete();
super.delete();
}

View file

@ -0,0 +1,62 @@
package dev.engine_room.flywheel.backend.engine.instancing;
import org.lwjgl.opengl.GL32;
import org.lwjgl.system.MemoryUtil;
import dev.engine_room.flywheel.backend.Samplers;
import dev.engine_room.flywheel.backend.engine.embed.LightStorage;
import dev.engine_room.flywheel.backend.gl.TextureBuffer;
import dev.engine_room.flywheel.backend.gl.buffer.GlBuffer;
import dev.engine_room.flywheel.lib.memory.MemoryBlock;
public class InstancedLight {
private final GlBuffer lut;
private final GlBuffer sections;
private final TextureBuffer lutTexture;
private final TextureBuffer sectionsTexture;
public InstancedLight() {
sections = new GlBuffer();
lut = new GlBuffer();
lutTexture = new TextureBuffer(GL32.GL_R32UI);
sectionsTexture = new TextureBuffer(GL32.GL_R32UI);
}
public void bind() {
Samplers.LIGHT_LUT.makeActive();
lutTexture.bind(lut.handle());
Samplers.LIGHT_SECTIONS.makeActive();
sectionsTexture.bind(sections.handle());
}
public void flush(LightStorage light) {
if (light.capacity() == 0) {
return;
}
light.upload(sections);
if (light.checkNeedsLutRebuildAndClear()) {
var lut = light.createLut();
var up = MemoryBlock.malloc((long) lut.size() * Integer.BYTES);
long ptr = up.ptr();
for (int i = 0; i < lut.size(); i++) {
MemoryUtil.memPutInt(ptr + (long) Integer.BYTES * i, lut.getInt(i));
}
this.lut.upload(up);
up.free();
}
}
public void delete() {
sections.delete();
lut.delete();
lutTexture.delete();
sectionsTexture.delete();
}
}

View file

@ -5,14 +5,20 @@ import org.lwjgl.opengl.GL32;
public class TextureBuffer extends GlObject {
public static final int MAX_TEXELS = GL32.glGetInteger(GL32.GL_MAX_TEXTURE_BUFFER_SIZE);
public static final int MAX_BYTES = MAX_TEXELS * 16; // 4 channels * 4 bytes
private final int format;
public TextureBuffer() {
this(GL32.GL_RGBA32UI);
}
public TextureBuffer(int format) {
handle(GL32.glGenTextures());
this.format = format;
}
public void bind(int buffer) {
GL32.glBindTexture(GL32.GL_TEXTURE_BUFFER, handle());
GL32.glTexBuffer(GL32.GL_TEXTURE_BUFFER, GL32.GL_RGBA32UI, buffer);
GL32.glTexBuffer(GL32.GL_TEXTURE_BUFFER, format, buffer);
}
@Override

View file

@ -13,9 +13,7 @@ uniform sampler2D _flw_crumblingTex;
in vec2 _flw_crumblingTexCoord;
#endif
#ifdef _FLW_EMBEDDED
bool _flw_embeddedLight(vec3 worldPos, out vec2 lightCoord);
#endif
flat in uint _flw_instanceID;

View file

@ -3,5 +3,5 @@
#define _FLW_MODEL_INDEX_BUFFER_BINDING 2
#define _FLW_MODEL_BUFFER_BINDING 3
#define _FLW_DRAW_BUFFER_BINDING 4
#define _FLW_EMBEDDING_LUT_BINDING 5
#define _FLW_EMBEDDING_LIGHT_BINDING 6
#define _FLW_LIGHT_LUT_BINDING 5
#define _FLW_LIGHT_SECTIONS_BINDING 6

View file

@ -1,131 +1,25 @@
#include "flywheel:internal/common.frag"
#include "flywheel:internal/light_lut.glsl"
#include "flywheel:internal/indirect/buffer_bindings.glsl"
flat in uvec3 _flw_packedMaterial;
#ifdef _FLW_EMBEDDED
layout(std430, binding = _FLW_EMBEDDING_LUT_BINDING) restrict readonly buffer EmbeddingLut {
uint _flw_embeddingLut[];
layout(std430, binding = _FLW_LIGHT_LUT_BINDING) restrict readonly buffer LightLut {
uint _flw_lightLut[];
};
const uint _FLW_LIGHT_SECTION_SIZE_BYTES = 18 * 18 * 18;
const uint _FLW_LIGHT_SECTION_SIZE_INTS = _FLW_LIGHT_SECTION_SIZE_BYTES / 4;
layout(std430, binding = _FLW_EMBEDDING_LIGHT_BINDING) restrict readonly buffer LightSections {
layout(std430, binding = _FLW_LIGHT_SECTIONS_BINDING) restrict readonly buffer LightSections {
uint _flw_lightSections[];
};
/// 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.
/// @param next Output. The index of the next step in the LUT.
/// @return true if the coordinate is not in the span.
bool _flw_nextLut(uint base, int coord, out uint next) {
// The base coordinate.
int start = int(_flw_embeddingLut[base]);
// The width of the coordinate span.
uint size = _flw_embeddingLut[base + 1];
// Index of the coordinate in the span.
int i = coord - start;
if (i < 0 || i >= size) {
// We missed.
return true;
uint _flw_indexLut(uint index) {
return _flw_lightLut[index];
}
next = _flw_embeddingLut[base + 2 + i];
return false;
uint _flw_indexLight(uint index) {
return _flw_lightSections[index];
}
bool _flw_chunkCoordToSectionIndex(ivec3 sectionPos, out uint index) {
uint y;
if (_flw_nextLut(0, sectionPos.x, y) || y == 0) {
return true;
}
uint z;
if (_flw_nextLut(y, sectionPos.y, z) || z == 0) {
return true;
}
uint sectionIndex;
if (_flw_nextLut(z, sectionPos.z, sectionIndex) || sectionIndex == 0) {
return true;
}
// The index is written as 1-based so we can properly detect missing sections.
index = sectionIndex - 1;
return false;
}
vec2 _flw_lightAt(uint sectionOffset, uvec3 blockInSectionPos) {
uint byteOffset = blockInSectionPos.x + blockInSectionPos.z * 18u + blockInSectionPos.y * 18u * 18u;
uint uintOffset = byteOffset >> 2u;
uint bitOffset = (byteOffset & 3u) << 3;
uint raw = _flw_lightSections[sectionOffset + uintOffset];
uint block = (raw >> bitOffset) & 0xFu;
uint sky = (raw >> (bitOffset + 4u)) & 0xFu;
return vec2(block, sky);
}
bool _flw_embeddedLight(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));
uint lightSectionIndex;
if (_flw_chunkCoordToSectionIndex(blockPos >> 4, lightSectionIndex)) {
// TODO: useful debug mode for this.
// flw_fragOverlay = ivec2(0, 3);
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 = _flw_lightAt(sectionOffset, lowestCorner);
vec2 light001 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 0, 1));
vec2 light010 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 1, 0));
vec2 light011 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 1, 1));
vec2 light100 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 0, 0));
vec2 light101 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 0, 1));
vec2 light110 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 1, 0));
vec2 light111 = _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;
}
#endif
void main() {
_flw_uberMaterialFragmentIndex = _flw_packedMaterial.x;
_flw_unpackUint2x16(_flw_packedMaterial.y, _flw_uberCutoutIndex, _flw_uberFogIndex);

View file

@ -1,12 +1,18 @@
#include "flywheel:internal/common.frag"
#include "flywheel:internal/light_lut.glsl"
uniform uvec4 _flw_packedMaterial;
#ifdef _FLW_EMBEDDED
bool _flw_embeddedLight(vec3 worldPos, out vec2 lightCoord) {
return true;
uniform usamplerBuffer _flw_lightLut;
uniform usamplerBuffer _flw_lightSections;
uint _flw_indexLut(uint index) {
return texelFetch(_flw_lightLut, int(index)).r;
}
uint _flw_indexLight(uint index) {
return texelFetch(_flw_lightSections, int(index)).r;
}
#endif
void main() {
_flw_uberMaterialFragmentIndex = _flw_packedMaterial.y;

View file

@ -0,0 +1,115 @@
const uint _FLW_LIGHT_SECTION_SIZE_BYTES = 18 * 18 * 18;
const uint _FLW_LIGHT_SECTION_SIZE_INTS = _FLW_LIGHT_SECTION_SIZE_BYTES / 4;
uint _flw_indexLut(uint index);
uint _flw_indexLight(uint index);
/// 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.
/// @param next Output. The index of the next step in the LUT.
/// @return true if the coordinate is not in the span.
bool _flw_nextLut(uint base, int coord, out uint next) {
// The base coordinate.
int start = int(_flw_indexLut(base));
// The width of the coordinate span.
uint size = _flw_indexLut(base + 1);
// Index of the coordinate in the span.
int i = coord - start;
if (i < 0 || i >= size) {
// We missed.
return true;
}
next = _flw_indexLut(base + 2 + i);
return false;
}
bool _flw_chunkCoordToSectionIndex(ivec3 sectionPos, out uint index) {
uint y;
if (_flw_nextLut(0, sectionPos.x, y) || y == 0) {
return true;
}
uint z;
if (_flw_nextLut(y, sectionPos.y, z) || z == 0) {
return true;
}
uint sectionIndex;
if (_flw_nextLut(z, sectionPos.z, sectionIndex) || sectionIndex == 0) {
return true;
}
// The index is written as 1-based so we can properly detect missing sections.
index = sectionIndex - 1;
return false;
}
vec2 _flw_lightAt(uint sectionOffset, uvec3 blockInSectionPos) {
uint byteOffset = blockInSectionPos.x + blockInSectionPos.z * 18u + blockInSectionPos.y * 18u * 18u;
uint uintOffset = byteOffset >> 2u;
uint bitOffset = (byteOffset & 3u) << 3;
uint raw = _flw_indexLight(sectionOffset + uintOffset);
uint block = (raw >> bitOffset) & 0xFu;
uint sky = (raw >> (bitOffset + 4u)) & 0xFu;
return vec2(block, sky);
}
bool _flw_embeddedLight(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));
uint lightSectionIndex;
if (_flw_chunkCoordToSectionIndex(blockPos >> 4, lightSectionIndex)) {
// TODO: useful debug mode for this.
// flw_fragOverlay = ivec2(0, 3);
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 = _flw_lightAt(sectionOffset, lowestCorner);
vec2 light001 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 0, 1));
vec2 light010 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 1, 0));
vec2 light011 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 1, 1));
vec2 light100 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 0, 0));
vec2 light101 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 0, 1));
vec2 light110 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 1, 0));
vec2 light111 = _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;
}