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.
This commit is contained in:
Jozufozu 2024-01-27 15:25:48 -08:00
parent fb95556623
commit a04c3f52b2
8 changed files with 414 additions and 1 deletions

View File

@ -66,6 +66,29 @@ public final class InstanceTypes {
.cullShader(Flywheel.rl("instance/cull/oriented.glsl")) .cullShader(Flywheel.rl("instance/cull/oriented.glsl"))
.register(); .register();
public static final InstanceType<ShadowInstance> 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() { private InstanceTypes() {
} }

View File

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

View File

@ -33,11 +33,13 @@ import net.minecraft.world.phys.Vec3;
public abstract class AbstractEntityVisual<T extends Entity> extends AbstractVisual implements EntityVisual<T> { public abstract class AbstractEntityVisual<T extends Entity> extends AbstractVisual implements EntityVisual<T> {
protected final T entity; protected final T entity;
protected final EntityVisibilityTester visibilityTester; protected final EntityVisibilityTester visibilityTester;
protected final ShadowComponent shadow;
public AbstractEntityVisual(VisualizationContext ctx, T entity) { public AbstractEntityVisual(VisualizationContext ctx, T entity) {
super(ctx, entity.level()); super(ctx, entity.level());
this.entity = entity; this.entity = entity;
visibilityTester = new EntityVisibilityTester(entity, ctx.renderOrigin(), 1.5f); visibilityTester = new EntityVisibilityTester(entity, ctx.renderOrigin(), 1.5f);
shadow = new ShadowComponent(ctx, entity);
} }
@Override @Override
@ -87,4 +89,9 @@ public abstract class AbstractEntityVisual<T extends Entity> extends AbstractVis
public boolean isVisible(FrustumIntersection frustum) { public boolean isVisible(FrustumIntersection frustum) {
return entity.noCulling || visibilityTester.check(frustum); return entity.noCulling || visibilityTester.check(frustum);
} }
@Override
protected void _delete() {
shadow.delete();
}
} }

View File

@ -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.
* <br>
* 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 <I> The type of instance to recycle.
*/
public class InstanceRecycler<I extends Instance> {
private final Supplier<I> factory;
private final List<I> instances = new ArrayList<>();
private int count;
public InstanceRecycler(Supplier<I> factory) {
this.factory = factory;
}
/**
* Reset the count of instances.
* <br>
* 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.
* <br>
* 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.
* <br>
* 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();
}
}

View File

@ -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<ShadowInstance> 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<Material, Mesh> meshes = ImmutableMap.of(MATERIAL, ShadowMesh.INSTANCE);
private ShadowModel() {
}
@Override
public Map<Material, Mesh> 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).
* <br>
* 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);
}
}
}
}

View File

@ -1,5 +1,7 @@
package com.jozufozu.flywheel.vanilla; package com.jozufozu.flywheel.vanilla;
import org.jetbrains.annotations.Nullable;
import com.jozufozu.flywheel.api.event.RenderStage; import com.jozufozu.flywheel.api.event.RenderStage;
import com.jozufozu.flywheel.api.visual.DynamicVisual; import com.jozufozu.flywheel.api.visual.DynamicVisual;
import com.jozufozu.flywheel.api.visual.TickableVisual; import com.jozufozu.flywheel.api.visual.TickableVisual;
@ -37,6 +39,7 @@ public class MinecartVisual<T extends AbstractMinecart> extends AbstractEntityVi
private final ModelHolder bodyModel; private final ModelHolder bodyModel;
private TransformedInstance body; private TransformedInstance body;
@Nullable
private TransformedInstance contents; private TransformedInstance contents;
private BlockState blockState; private BlockState blockState;
private boolean active; private boolean active;
@ -46,6 +49,7 @@ public class MinecartVisual<T extends AbstractMinecart> extends AbstractEntityVi
public MinecartVisual(VisualizationContext ctx, T entity, ModelHolder bodyModel) { public MinecartVisual(VisualizationContext ctx, T entity, ModelHolder bodyModel) {
super(ctx, entity); super(ctx, entity);
this.bodyModel = bodyModel; this.bodyModel = bodyModel;
shadow.radius(0.7f);
} }
private static ModelHolder createBodyModelHolder(ModelLayerLocation layer) { private static ModelHolder createBodyModelHolder(ModelLayerLocation layer) {
@ -70,6 +74,7 @@ public class MinecartVisual<T extends AbstractMinecart> extends AbstractEntityVi
.createInstance(); .createInstance();
} }
@Nullable
private TransformedInstance createContentsInstance() { private TransformedInstance createContentsInstance() {
RenderShape shape = blockState.getRenderShape(); RenderShape shape = blockState.getRenderShape();
@ -94,7 +99,9 @@ public class MinecartVisual<T extends AbstractMinecart> extends AbstractEntityVi
if (displayBlockState != blockState) { if (displayBlockState != blockState) {
blockState = displayBlockState; blockState = displayBlockState;
if (contents != null) {
contents.delete(); contents.delete();
}
contents = createContentsInstance(); contents = createContentsInstance();
} }
@ -103,6 +110,8 @@ public class MinecartVisual<T extends AbstractMinecart> extends AbstractEntityVi
@Override @Override
public void beginFrame(VisualFrameContext context) { public void beginFrame(VisualFrameContext context) {
shadow.beginFrame(context);
if (!isVisible(context.frustum())) { if (!isVisible(context.frustum())) {
return; return;
} }
@ -200,6 +209,7 @@ public class MinecartVisual<T extends AbstractMinecart> extends AbstractEntityVi
if (contents != null) { if (contents != null) {
contents.delete(); contents.delete();
} }
super._delete();
} }
public static boolean shouldSkipRender(AbstractMinecart minecart) { public static boolean shouldSkipRender(AbstractMinecart minecart) {

View File

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

View File

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