From a04c3f52b22dc21927663e220b1e96c04df639ab Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Sat, 27 Jan 2024 15:25:48 -0800 Subject: [PATCH] Throwing shade - Add support for entity shadows. - Create instance type specifically for shadows. - Add ShadowComponent utility which emulates what EntityRenderDispatcher does, but based on top of our instance system. - Add some missing nullability annotations to MinecartVisual. - Create InstanceRecycler utility for managing dynamic numbers of instances per frame. --- .../flywheel/lib/instance/InstanceTypes.java | 23 ++ .../flywheel/lib/instance/ShadowInstance.java | 17 ++ .../lib/visual/AbstractEntityVisual.java | 7 + .../flywheel/lib/visual/InstanceRecycler.java | 74 +++++ .../flywheel/lib/visual/ShadowComponent.java | 267 ++++++++++++++++++ .../flywheel/vanilla/MinecartVisual.java | 12 +- .../flywheel/instance/cull/shadow.glsl | 5 + .../flywheel/flywheel/instance/shadow.vert | 10 + 8 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/jozufozu/flywheel/lib/instance/ShadowInstance.java create mode 100644 src/main/java/com/jozufozu/flywheel/lib/visual/InstanceRecycler.java create mode 100644 src/main/java/com/jozufozu/flywheel/lib/visual/ShadowComponent.java create mode 100644 src/main/resources/assets/flywheel/flywheel/instance/cull/shadow.glsl create mode 100644 src/main/resources/assets/flywheel/flywheel/instance/shadow.vert diff --git a/src/main/java/com/jozufozu/flywheel/lib/instance/InstanceTypes.java b/src/main/java/com/jozufozu/flywheel/lib/instance/InstanceTypes.java index edf973519..b89aea074 100644 --- a/src/main/java/com/jozufozu/flywheel/lib/instance/InstanceTypes.java +++ b/src/main/java/com/jozufozu/flywheel/lib/instance/InstanceTypes.java @@ -66,6 +66,29 @@ public final class InstanceTypes { .cullShader(Flywheel.rl("instance/cull/oriented.glsl")) .register(); + public static final InstanceType SHADOW = SimpleInstanceType.builder(ShadowInstance::new) + .layout(LayoutBuilder.create() + .vector("pos", FloatRepr.FLOAT, 3) + .vector("entityPosXZ", FloatRepr.FLOAT, 2) + .vector("size", FloatRepr.FLOAT, 2) + .scalar("alpha", FloatRepr.FLOAT) + .scalar("radius", FloatRepr.FLOAT) + .build()) + .writer((ptr, instance) -> { + MemoryUtil.memPutFloat(ptr, instance.x); + MemoryUtil.memPutFloat(ptr + 4, instance.y); + MemoryUtil.memPutFloat(ptr + 8, instance.z); + MemoryUtil.memPutFloat(ptr + 12, instance.entityX); + MemoryUtil.memPutFloat(ptr + 16, instance.entityZ); + MemoryUtil.memPutFloat(ptr + 20, instance.sizeX); + MemoryUtil.memPutFloat(ptr + 24, instance.sizeZ); + MemoryUtil.memPutFloat(ptr + 28, instance.alpha); + MemoryUtil.memPutFloat(ptr + 32, instance.radius); + }) + .vertexShader(Flywheel.rl("instance/shadow.vert")) + .cullShader(Flywheel.rl("instance/cull/shadow.glsl")) + .register(); + private InstanceTypes() { } diff --git a/src/main/java/com/jozufozu/flywheel/lib/instance/ShadowInstance.java b/src/main/java/com/jozufozu/flywheel/lib/instance/ShadowInstance.java new file mode 100644 index 000000000..6b1cb9338 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/lib/instance/ShadowInstance.java @@ -0,0 +1,17 @@ +package com.jozufozu.flywheel.lib.instance; + +import com.jozufozu.flywheel.api.instance.InstanceHandle; +import com.jozufozu.flywheel.api.instance.InstanceType; + +public class ShadowInstance extends AbstractInstance { + + public float x, y, z; + public float entityX, entityZ; + public float sizeX, sizeZ; + public float alpha; + public float radius; + + protected ShadowInstance(InstanceType type, InstanceHandle handle) { + super(type, handle); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/lib/visual/AbstractEntityVisual.java b/src/main/java/com/jozufozu/flywheel/lib/visual/AbstractEntityVisual.java index ebba08ccb..2870d4d37 100644 --- a/src/main/java/com/jozufozu/flywheel/lib/visual/AbstractEntityVisual.java +++ b/src/main/java/com/jozufozu/flywheel/lib/visual/AbstractEntityVisual.java @@ -33,11 +33,13 @@ import net.minecraft.world.phys.Vec3; public abstract class AbstractEntityVisual extends AbstractVisual implements EntityVisual { protected final T entity; protected final EntityVisibilityTester visibilityTester; + protected final ShadowComponent shadow; public AbstractEntityVisual(VisualizationContext ctx, T entity) { super(ctx, entity.level()); this.entity = entity; visibilityTester = new EntityVisibilityTester(entity, ctx.renderOrigin(), 1.5f); + shadow = new ShadowComponent(ctx, entity); } @Override @@ -87,4 +89,9 @@ public abstract class AbstractEntityVisual extends AbstractVis public boolean isVisible(FrustumIntersection frustum) { return entity.noCulling || visibilityTester.check(frustum); } + + @Override + protected void _delete() { + shadow.delete(); + } } diff --git a/src/main/java/com/jozufozu/flywheel/lib/visual/InstanceRecycler.java b/src/main/java/com/jozufozu/flywheel/lib/visual/InstanceRecycler.java new file mode 100644 index 000000000..c848289e5 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/lib/visual/InstanceRecycler.java @@ -0,0 +1,74 @@ +package com.jozufozu.flywheel.lib.visual; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import com.jozufozu.flywheel.api.instance.Instance; + +/** + * A utility for recycling instances. + *
+ * If the exact number of instances you need each frame is unknown, you can use this to manage + * a pool of instances that will be dynamically created, deleted, or re-used as necessary. + * + * @param The type of instance to recycle. + */ +public class InstanceRecycler { + + private final Supplier factory; + + private final List instances = new ArrayList<>(); + + private int count; + + public InstanceRecycler(Supplier factory) { + this.factory = factory; + } + + /** + * Reset the count of instances. + *
+ * Call this at before your first call to {@link #get()} each frame. + */ + public void resetCount() { + count = 0; + } + + /** + * Get the next instance in the pool, creating a new one if necessary. + *
+ * The returned instance may not be in its default state. + * + * @return The next instance in the pool. + */ + public I get() { + var lastCount = count++; + if (lastCount < instances.size()) { + return instances.get(lastCount); + } else { + var out = factory.get(); + instances.add(out); + return out; + } + } + + /** + * Delete any instances that were not used this frame. + *
+ * Call this after your last call to {@link #get()} each frame. + */ + public void discardExtra() { + for (int i = count; i < instances.size(); i++) { + instances.get(i) + .delete(); + } + instances.subList(count, instances.size()) + .clear(); + } + + public void delete() { + instances.forEach(Instance::delete); + instances.clear(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/lib/visual/ShadowComponent.java b/src/main/java/com/jozufozu/flywheel/lib/visual/ShadowComponent.java new file mode 100644 index 000000000..f95d93d64 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/lib/visual/ShadowComponent.java @@ -0,0 +1,267 @@ +package com.jozufozu.flywheel.lib.visual; + +import java.util.Map; + +import org.jetbrains.annotations.Nullable; +import org.joml.Vector4f; +import org.joml.Vector4fc; + +import com.google.common.collect.ImmutableMap; +import com.jozufozu.flywheel.api.event.RenderStage; +import com.jozufozu.flywheel.api.material.Material; +import com.jozufozu.flywheel.api.material.Transparency; +import com.jozufozu.flywheel.api.material.WriteMask; +import com.jozufozu.flywheel.api.model.Mesh; +import com.jozufozu.flywheel.api.model.Model; +import com.jozufozu.flywheel.api.vertex.MutableVertexList; +import com.jozufozu.flywheel.api.visual.VisualFrameContext; +import com.jozufozu.flywheel.api.visualization.VisualizationContext; +import com.jozufozu.flywheel.lib.instance.InstanceTypes; +import com.jozufozu.flywheel.lib.instance.ShadowInstance; +import com.jozufozu.flywheel.lib.material.SimpleMaterial; +import com.jozufozu.flywheel.lib.model.QuadMesh; + +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.VoxelShape; + +public class ShadowComponent { + + private final VisualizationContext context; + private final LevelReader level; + private final Entity entity; + private final InstanceRecycler instances = new InstanceRecycler<>(this::instance); + private final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + + private float radius = 0.5F; + private float strength = 1.0F; + private boolean enabled = true; + + public ShadowComponent(VisualizationContext context, Entity entity) { + this.context = context; + this.level = entity.level(); + this.entity = entity; + } + + public void radius(float radius) { + this.radius = radius; + } + + public void strength(float weight) { + this.strength = weight; + } + + public void enabled(boolean enabled) { + this.enabled = enabled; + + if (!enabled) { + instances.delete(); + } + } + + public void beginFrame(VisualFrameContext context) { + if (!enabled) { + return; + } + + instances.resetCount(); + + double entityX = Mth.lerp(context.partialTick(), entity.xOld, entity.getX()); + double entityY = Mth.lerp(context.partialTick(), entity.yOld, entity.getY()); + double entityZ = Mth.lerp(context.partialTick(), entity.zOld, entity.getZ()); + float castDistance = Math.min(strength / 0.5F, radius); + int minXPos = Mth.floor(entityX - (double) radius); + int maxXPos = Mth.floor(entityX + (double) radius); + int minYPos = Mth.floor(entityY - (double) castDistance); + int maxYPos = Mth.floor(entityY); + int minZPos = Mth.floor(entityZ - (double) radius); + int maxZPos = Mth.floor(entityZ + (double) radius); + + for (int z = minZPos; z <= maxZPos; ++z) { + for (int x = minXPos; x <= maxXPos; ++x) { + pos.set(x, 0, z); + ChunkAccess chunkaccess = level.getChunk(pos); + + for (int y = minYPos; y <= maxYPos; ++y) { + pos.setY(y); + float actualWeight = strength - (float) (entityY - pos.getY()) * 0.5F; + maybeSetupShadowInstance(chunkaccess, (float) entityX, (float) entityZ, actualWeight); + } + } + } + + instances.discardExtra(); + } + + private void maybeSetupShadowInstance(ChunkAccess pChunk, float entityX, float entityZ, float weight) { + // TODO: cache this? + var maxLocalRawBrightness = level.getMaxLocalRawBrightness(pos); + if (maxLocalRawBrightness <= 3) { + // Too dark to render. + return; + } + float blockBrightness = LightTexture.getBrightness(level.dimensionType(), maxLocalRawBrightness); + float alpha = weight * 0.5F * blockBrightness; + if (!(alpha >= 0.0F)) { + // Too far away/too weak to render. + return; + } + if (alpha > 1.0F) { + alpha = 1.0F; + } + + // Grab the AABB for the block below the current position. + pos.setY(pos.getY() - 1); + var aabb = getAabbForPos(pChunk, pos); + if (aabb == null) { + // No aabb means the block shouldn't receive a shadow. + return; + } + + var renderOrigin = context.renderOrigin(); + int x = pos.getX() - renderOrigin.getX(); + int y = pos.getY() - renderOrigin.getY() + 1; // +1 since we moved the pos down. + int z = pos.getZ() - renderOrigin.getZ(); + + double minX = x + aabb.minX; + double minY = y + aabb.minY; + double minZ = z + aabb.minZ; + double maxX = x + aabb.maxX; + double maxZ = z + aabb.maxZ; + + var instance = instances.get(); + instance.x = (float) minX; + instance.y = (float) minY; + instance.z = (float) minZ; + instance.entityX = entityX; + instance.entityZ = entityZ; + instance.sizeX = (float) (maxX - minX); + instance.sizeZ = (float) (maxZ - minZ); + instance.alpha = alpha; + instance.radius = this.radius; + instance.setChanged(); + } + + @Nullable + private AABB getAabbForPos(ChunkAccess pChunk, BlockPos pos) { + BlockState blockstate = pChunk.getBlockState(pos); + if (blockstate.getRenderShape() == RenderShape.INVISIBLE) { + return null; + } + if (!blockstate.isCollisionShapeFullBlock(pChunk, pos)) { + return null; + } + VoxelShape voxelshape = blockstate.getShape(pChunk, pos); + if (voxelshape.isEmpty()) { + return null; + } + return voxelshape.bounds(); + } + + private ShadowInstance instance() { + return context.instancerProvider() + .instancer(InstanceTypes.SHADOW, ShadowModel.INSTANCE, RenderStage.AFTER_ENTITIES) + .createInstance(); + } + + public void delete() { + instances.delete(); + } + + private static class ShadowModel implements Model { + public static final ShadowModel INSTANCE = new ShadowModel(); + public static final Material MATERIAL = SimpleMaterial.builder() + .transparency(Transparency.TRANSLUCENT) + .writeMask(WriteMask.COLOR) + .polygonOffset(true) // vanilla shadows use "view offset" but this seems to work fine + .texture(new ResourceLocation("minecraft", "textures/misc/shadow.png")) + .build(); + private static final Vector4fc BOUNDING_SPHERE = new Vector4f(0.5f, 0, 0.5f, (float) (Math.sqrt(2) * 0.5)); + private static final ImmutableMap meshes = ImmutableMap.of(MATERIAL, ShadowMesh.INSTANCE); + + private ShadowModel() { + } + + @Override + public Map meshes() { + return meshes; + } + + @Override + public Vector4fc boundingSphere() { + return BOUNDING_SPHERE; + } + + @Override + public int vertexCount() { + return ShadowMesh.INSTANCE.vertexCount(); + } + + @Override + public void delete() { + + } + + /** + * A single quad extending from the origin to (1, 0, 1). + *
+ * To be scaled and translated to the correct position and size. + */ + private static class ShadowMesh implements QuadMesh { + public static final ShadowMesh INSTANCE = new ShadowMesh(); + + private ShadowMesh() { + } + + @Override + public int vertexCount() { + return 4; + } + + @Override + public void write(MutableVertexList vertexList) { + writeVertex(vertexList, 0, 0, 0); + writeVertex(vertexList, 1, 0, 1); + writeVertex(vertexList, 2, 1, 1); + writeVertex(vertexList, 3, 1, 0); + } + + @Override + public Vector4fc boundingSphere() { + return BOUNDING_SPHERE; + } + + @Override + public void delete() { + + } + + // Magic numbers taken from: + // net.minecraft.client.renderer.entity.EntityRenderDispatcher#shadowVertex + private static void writeVertex(MutableVertexList vertexList, int i, float x, float z) { + vertexList.x(i, x); + vertexList.y(i, 0); + vertexList.z(i, z); + vertexList.r(i, 1); + vertexList.g(i, 1); + vertexList.b(i, 1); + vertexList.u(i, 0); + vertexList.v(i, 0); + vertexList.light(i, 15728880); + vertexList.normalX(i, 0); + vertexList.normalY(i, 1); + vertexList.normalZ(i, 0); + + } + } + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/vanilla/MinecartVisual.java b/src/main/java/com/jozufozu/flywheel/vanilla/MinecartVisual.java index 8fe40c5de..0136a44f1 100644 --- a/src/main/java/com/jozufozu/flywheel/vanilla/MinecartVisual.java +++ b/src/main/java/com/jozufozu/flywheel/vanilla/MinecartVisual.java @@ -1,5 +1,7 @@ package com.jozufozu.flywheel.vanilla; +import org.jetbrains.annotations.Nullable; + import com.jozufozu.flywheel.api.event.RenderStage; import com.jozufozu.flywheel.api.visual.DynamicVisual; import com.jozufozu.flywheel.api.visual.TickableVisual; @@ -37,6 +39,7 @@ public class MinecartVisual extends AbstractEntityVi private final ModelHolder bodyModel; private TransformedInstance body; + @Nullable private TransformedInstance contents; private BlockState blockState; private boolean active; @@ -46,6 +49,7 @@ public class MinecartVisual extends AbstractEntityVi public MinecartVisual(VisualizationContext ctx, T entity, ModelHolder bodyModel) { super(ctx, entity); this.bodyModel = bodyModel; + shadow.radius(0.7f); } private static ModelHolder createBodyModelHolder(ModelLayerLocation layer) { @@ -70,6 +74,7 @@ public class MinecartVisual extends AbstractEntityVi .createInstance(); } + @Nullable private TransformedInstance createContentsInstance() { RenderShape shape = blockState.getRenderShape(); @@ -94,7 +99,9 @@ public class MinecartVisual extends AbstractEntityVi if (displayBlockState != blockState) { blockState = displayBlockState; - contents.delete(); + if (contents != null) { + contents.delete(); + } contents = createContentsInstance(); } @@ -103,6 +110,8 @@ public class MinecartVisual extends AbstractEntityVi @Override public void beginFrame(VisualFrameContext context) { + shadow.beginFrame(context); + if (!isVisible(context.frustum())) { return; } @@ -200,6 +209,7 @@ public class MinecartVisual extends AbstractEntityVi if (contents != null) { contents.delete(); } + super._delete(); } public static boolean shouldSkipRender(AbstractMinecart minecart) { diff --git a/src/main/resources/assets/flywheel/flywheel/instance/cull/shadow.glsl b/src/main/resources/assets/flywheel/flywheel/instance/cull/shadow.glsl new file mode 100644 index 000000000..ee0afb8dd --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/instance/cull/shadow.glsl @@ -0,0 +1,5 @@ +void flw_transformBoundingSphere(in FlwInstance i, inout vec3 center, inout float radius) { + // We can just ignore the base center/radius. + center = i.pos + vec3(i.size.x * 0.5, 0., i.size.y * 0.5); + radius = max(i.size.x, i.size.y) * 0.5; +} diff --git a/src/main/resources/assets/flywheel/flywheel/instance/shadow.vert b/src/main/resources/assets/flywheel/flywheel/instance/shadow.vert new file mode 100644 index 000000000..97649f624 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/instance/shadow.vert @@ -0,0 +1,10 @@ +void flw_instanceVertex(in FlwInstance i) { + // Stretch the quad to the shape of the block the shadow is being cast on, + // then move it to the correct position. + flw_vertexPos.xyz = flw_vertexPos.xyz * vec3(i.size.x, 1., i.size.y) + i.pos; + + // Uvs are calculated based on the distance to the entity. + flw_vertexTexCoord = (flw_vertexPos.xz - i.entityPosXZ) * 0.5 / i.radius + 0.5; + + flw_vertexColor.a = i.alpha; +}