Use actual projection rendering for projectors.
This commit is contained in:
@@ -1,16 +1,12 @@
|
||||
package li.cil.oc2.client.renderer;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import li.cil.oc2.api.API;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public abstract class ModRenderType extends RenderType {
|
||||
@@ -38,7 +34,6 @@ public abstract class ModRenderType extends RenderType {
|
||||
true,
|
||||
CompositeState.builder()
|
||||
.setShaderState(RENDERTYPE_LIGHTNING_SHADER)
|
||||
.setOutputState(WEATHER_TARGET)
|
||||
.setTransparencyState(LIGHTNING_TRANSPARENCY)
|
||||
.setWriteMaskState(COLOR_WRITE)
|
||||
.setCullState(NO_CULL)
|
||||
@@ -59,7 +54,6 @@ public abstract class ModRenderType extends RenderType {
|
||||
final RenderType.CompositeState state = RenderType.CompositeState.builder()
|
||||
.setShaderState(POSITION_TEX_SHADER)
|
||||
.setTextureState(texture)
|
||||
.setOutputState(TRANSLUCENT_TARGET)
|
||||
.setTransparencyState(ADDITIVE_TRANSPARENCY)
|
||||
.setCullState(NO_CULL)
|
||||
.createCompositeState(false);
|
||||
@@ -91,38 +85,8 @@ public abstract class ModRenderType extends RenderType {
|
||||
state);
|
||||
}
|
||||
|
||||
public static RenderType getProjector(final DynamicTexture texture) {
|
||||
final RenderType.CompositeState state = RenderType.CompositeState.builder()
|
||||
.setShaderState(POSITION_TEX_SHADER)
|
||||
.setTextureState(new DynamicTextureStateShard(texture))
|
||||
.setOutputState(TRANSLUCENT_TARGET)
|
||||
.setTransparencyState(ADDITIVE_TRANSPARENCY)
|
||||
.setCullState(NO_CULL)
|
||||
.createCompositeState(false);
|
||||
return create(
|
||||
API.MOD_ID + "/projector",
|
||||
DefaultVertexFormat.POSITION_TEX,
|
||||
VertexFormat.Mode.QUADS,
|
||||
256,
|
||||
false,
|
||||
true,
|
||||
state);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
private static final class DynamicTextureStateShard extends EmptyTextureStateShard {
|
||||
public DynamicTextureStateShard(final DynamicTexture texture) {
|
||||
super(() -> {
|
||||
RenderSystem.enableTexture();
|
||||
// TODO client side setting
|
||||
// texture.setFilter(true, false);
|
||||
RenderSystem.setShaderTexture(0, texture.getId());
|
||||
}, () -> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private ModRenderType(final String name, final VertexFormat format, final VertexFormat.Mode drawMode, final int bufferSize, final boolean useDelegate, final boolean needsSorting, final Runnable setupTask, final Runnable clearTask) {
|
||||
super(name, format, drawMode, bufferSize, useDelegate, needsSorting, setupTask, clearTask);
|
||||
}
|
||||
|
||||
66
src/main/java/li/cil/oc2/client/renderer/ModShaders.java
Normal file
66
src/main/java/li/cil/oc2/client/renderer/ModShaders.java
Normal file
@@ -0,0 +1,66 @@
|
||||
package li.cil.oc2.client.renderer;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.math.Matrix4f;
|
||||
import li.cil.oc2.api.API;
|
||||
import net.minecraft.client.renderer.ShaderInstance;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.client.event.RegisterShadersEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
|
||||
@Mod.EventBusSubscriber(value = Dist.CLIENT, modid = API.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
|
||||
public final class ModShaders {
|
||||
public static final int MAX_PROJECTORS = 3;
|
||||
|
||||
private static final ResourceLocation PROJECTORS_SHADER_LOCATION = new ResourceLocation(API.MOD_ID, "projectors");
|
||||
private static final String[] PROJECTOR_CAMERA_NAMES = {"ProjectorCamera0", "ProjectorCamera1", "ProjectorCamera2"};
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
private static ShaderInstance projectorsShader;
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
@Nullable
|
||||
public static ShaderInstance getProjectorsShader() {
|
||||
return projectorsShader;
|
||||
}
|
||||
|
||||
public static void configureProjectorsShader(
|
||||
final RenderTarget target,
|
||||
final Matrix4f inverseCameraMatrix,
|
||||
final DynamicTexture[] colors,
|
||||
final RenderTarget[] depths,
|
||||
final Matrix4f[] projectorCameraMatrices,
|
||||
final int count
|
||||
) {
|
||||
final int projectorCount = Math.min(count, MAX_PROJECTORS);
|
||||
projectorsShader.safeGetUniform("Count").set(projectorCount);
|
||||
|
||||
RenderSystem.setShaderTexture(0, target.getDepthTextureId());
|
||||
projectorsShader.safeGetUniform("InverseMainCamera").set(inverseCameraMatrix);
|
||||
|
||||
for (int i = 0; i < projectorCount; i++) {
|
||||
RenderSystem.setShaderTexture(1 + i * 2, colors[i].getId());
|
||||
RenderSystem.setShaderTexture(1 + i * 2 + 1, depths[i].getDepthTextureId());
|
||||
projectorsShader.safeGetUniform(PROJECTOR_CAMERA_NAMES[i]).set(projectorCameraMatrices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void handleRegisterShaders(final RegisterShadersEvent event) throws IOException {
|
||||
event.registerShader(new ShaderInstance(
|
||||
event.getResourceManager(),
|
||||
PROJECTORS_SHADER_LOCATION,
|
||||
DefaultVertexFormat.POSITION_TEX
|
||||
), instance -> projectorsShader = instance);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,28 @@
|
||||
package li.cil.oc2.client.renderer;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.RemovalNotification;
|
||||
import com.mojang.blaze3d.pipeline.MainTarget;
|
||||
import com.mojang.blaze3d.pipeline.RenderTarget;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import com.mojang.blaze3d.platform.TextureUtil;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.*;
|
||||
import com.mojang.math.Matrix3f;
|
||||
import com.mojang.math.Matrix4f;
|
||||
import com.mojang.math.Quaternion;
|
||||
import com.mojang.math.Vector3f;
|
||||
import li.cil.oc2.api.API;
|
||||
import li.cil.oc2.common.block.ProjectorBlock;
|
||||
import li.cil.oc2.common.blockentity.ProjectorBlockEntity;
|
||||
import li.cil.oc2.common.bus.device.vm.ProjectorVMDevice;
|
||||
import li.cil.oc2.common.ext.MinecraftExt;
|
||||
import li.cil.oc2.common.util.FakePlayerUtils;
|
||||
import li.cil.oc2.jcodec.common.model.Picture;
|
||||
import li.cil.oc2.jcodec.scale.Yuv420jToRgb;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
@@ -16,115 +30,146 @@ import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.client.renderer.FogRenderer;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
import net.minecraft.client.renderer.LevelRenderer;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraftforge.client.event.EntityViewRenderEvent;
|
||||
import net.minecraftforge.client.event.RenderGameOverlayEvent;
|
||||
import net.minecraftforge.client.event.RenderLevelLastEvent;
|
||||
import net.minecraftforge.client.event.RenderNameplateEvent;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.lwjgl.opengl.GL11.GL_NONE;
|
||||
import static org.lwjgl.opengl.GL11.glDrawBuffer;
|
||||
import static org.lwjgl.opengl.GL30.GL_FRAMEBUFFER;
|
||||
import static org.lwjgl.opengl.GL30.glBindFramebuffer;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = API.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
|
||||
public final class ProjectorDepthRenderer {
|
||||
private static final int DEPTH_CAPTURE_SIZE = 256;
|
||||
private static final float PROJECTOR_FOV = 50;
|
||||
private static final float PROJECTOR_ASPECT_RATIO = ProjectorVMDevice.WIDTH / (float) ProjectorVMDevice.HEIGHT;
|
||||
|
||||
private static final List<ProjectorBlockEntity> VISIBLE_PROJECTORS = new ArrayList<>();
|
||||
private static final DepthOnlyRenderTarget[] PROJECTOR_DEPTH_TARGETS = new DepthOnlyRenderTarget[ModShaders.MAX_PROJECTORS];
|
||||
private static final DynamicTexture[] PROJECTOR_COLOR_TARGETS = new DynamicTexture[ModShaders.MAX_PROJECTORS];
|
||||
private static final Matrix4f[] PROJECTOR_CAMERA_MATRICES = new Matrix4f[ModShaders.MAX_PROJECTORS];
|
||||
private static final Camera PROJECTOR_DEPTH_CAMERA = new Camera();
|
||||
private static final DepthOnlyRenderTarget MAIN_CAMERA_DEPTH = new DepthOnlyRenderTarget(MainTarget.DEFAULT_WIDTH, MainTarget.DEFAULT_HEIGHT);
|
||||
private static final float PROJECTOR_NEAR = 1 / 16f;
|
||||
private static final float PROJECTOR_FAR = 32f;
|
||||
private static final Matrix4f DEPTH_CAMERA_PROJECTION_MATRIX = Matrix4f.perspective(PROJECTOR_FOV, PROJECTOR_ASPECT_RATIO, PROJECTOR_NEAR, PROJECTOR_FAR);
|
||||
|
||||
private static final Cache<ProjectorBlockEntity, RenderInfo> RENDER_INFO = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(Duration.ofSeconds(5))
|
||||
.removalListener(ProjectorDepthRenderer::handleProjectorNoLongerRendering)
|
||||
.build();
|
||||
|
||||
private static final TextureTarget projectorDepthTarget = new TextureTarget(DEPTH_CAPTURE_SIZE, DEPTH_CAPTURE_SIZE, true, Minecraft.ON_OSX);
|
||||
private static final ProjectorDepthCamera camera = new ProjectorDepthCamera();
|
||||
private static boolean isRenderingProjectorDepth;
|
||||
private static HitResult hitResultBak;
|
||||
private static boolean entityShadowsBak;
|
||||
|
||||
private static void handleProjectorNoLongerRendering(final RemovalNotification<ProjectorBlockEntity, RenderInfo> notification) {
|
||||
final ProjectorBlockEntity projector = notification.getKey();
|
||||
if (projector != null) {
|
||||
projector.setFrameConsumer(null);
|
||||
}
|
||||
final RenderInfo renderInfo = notification.getValue();
|
||||
if (renderInfo != null) {
|
||||
renderInfo.close();
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
for (int i = 0; i < ModShaders.MAX_PROJECTORS; i++) {
|
||||
PROJECTOR_DEPTH_TARGETS[i] = new DepthOnlyRenderTarget(DEPTH_CAPTURE_SIZE, DEPTH_CAPTURE_SIZE);
|
||||
PROJECTOR_CAMERA_MATRICES[i] = new Matrix4f();
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Adds a projector that is being rendered this frame. This is called every frame a projector is rendering,
|
||||
* the list of rendering projectors is cleared at the end of every frame.
|
||||
*/
|
||||
public static void addProjector(final ProjectorBlockEntity projector) {
|
||||
VISIBLE_PROJECTORS.add(projector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we're currently rendering projector depth maps.
|
||||
* <p>
|
||||
* This is used in a couple of events and mixins, used to suppress regular rendering of things not needed in the
|
||||
* depth buffer.
|
||||
*/
|
||||
public static boolean isIsRenderingProjectorDepth() {
|
||||
return isRenderingProjectorDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from a mixin in the {@link LevelRenderer#renderLevel(PoseStack, float, long, boolean, Camera, GameRenderer, LightTexture, Matrix4f)}
|
||||
* method to grab the current depth buffer. This is necessary, because the depth buffer may be messed up by other
|
||||
* render passes when using the "Fabulous!" graphics mode.
|
||||
* <p>
|
||||
* Called before {@link #handleRenderLevelLast(RenderLevelLastEvent)} every frame.
|
||||
*/
|
||||
public static void captureMainCameraDepth() {
|
||||
final RenderTarget mainRenderTarget = Minecraft.getInstance().getMainRenderTarget();
|
||||
if (mainRenderTarget.width != MAIN_CAMERA_DEPTH.width || mainRenderTarget.height != MAIN_CAMERA_DEPTH.height) {
|
||||
MAIN_CAMERA_DEPTH.resize(mainRenderTarget.width, mainRenderTarget.height, Minecraft.ON_OSX);
|
||||
}
|
||||
|
||||
MAIN_CAMERA_DEPTH.copyDepthFrom(mainRenderTarget);
|
||||
mainRenderTarget.bindWrite(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the projected images of {@link ProjectorBlockEntity} instances that were registered via
|
||||
* {@link #addProjector(ProjectorBlockEntity)} this frame.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void handleRenderLevelLast(final RenderLevelLastEvent event) {
|
||||
final Minecraft minecraft = Minecraft.getInstance();
|
||||
if (!(minecraft instanceof MinecraftExt minecraftExt)) {
|
||||
if (VISIBLE_PROJECTORS.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ClientLevel level = minecraft.level;
|
||||
final LocalPlayer player = minecraft.player;
|
||||
if (level == null || player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isRenderingProjectorDepth = true;
|
||||
RenderSystem.backupProjectionMatrix();
|
||||
|
||||
final BlockPos blockPos = player.eyeBlockPosition();
|
||||
camera.configure(level, blockPos.relative(Direction.UP), player.getDirection().toYRot());
|
||||
minecraft.setCameraEntity(camera.getEntity());
|
||||
|
||||
final PoseStack viewModelStack;
|
||||
viewModelStack = new PoseStack();
|
||||
viewModelStack.mulPose(Vector3f.XP.rotationDegrees(camera.getXRot()));
|
||||
viewModelStack.mulPose(Vector3f.YP.rotationDegrees(camera.getYRot() + 180));
|
||||
|
||||
final Matrix3f viewRotationMatrix = viewModelStack.last().normal().copy();
|
||||
if (viewRotationMatrix.invert()) {
|
||||
RenderSystem.setInverseViewRotationMatrix(viewRotationMatrix);
|
||||
final Minecraft minecraft = Minecraft.getInstance();
|
||||
final ClientLevel level = minecraft.level;
|
||||
final LocalPlayer player = minecraft.player;
|
||||
if (level == null || player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
projectorDepthTarget.bindWrite(true);
|
||||
minecraftExt.setMainRenderTargetOverride(projectorDepthTarget);
|
||||
VISIBLE_PROJECTORS.sort((projector1, projector2) -> {
|
||||
final double distance1 = player.distanceToSqr(Vec3.atCenterOf(projector1.getBlockPos()));
|
||||
final double distance2 = player.distanceToSqr(Vec3.atCenterOf(projector2.getBlockPos()));
|
||||
return Double.compare(distance1, distance2);
|
||||
});
|
||||
|
||||
final Matrix4f projectionMatrix = Matrix4f.perspective(90, 1, 0.05f, 64f);
|
||||
RenderSystem.setProjectionMatrix(projectionMatrix);
|
||||
|
||||
final LevelRenderer levelRenderer = event.getLevelRenderer();
|
||||
levelRenderer.prepareCullFrustum(viewModelStack, camera.getPosition(), projectionMatrix);
|
||||
levelRenderer.renderLevel(
|
||||
viewModelStack,
|
||||
event.getPartialTick(),
|
||||
event.getStartNanos(),
|
||||
/* shouldRenderBlockOutline: */ false,
|
||||
camera,
|
||||
minecraft.gameRenderer,
|
||||
minecraft.gameRenderer.lightTexture(),
|
||||
projectionMatrix
|
||||
);
|
||||
final int projectorCount = Math.min(VISIBLE_PROJECTORS.size(), ModShaders.MAX_PROJECTORS);
|
||||
renderProjectorDepths(minecraft, level, player, event.getPartialTick(), event.getStartNanos(), projectorCount);
|
||||
renderProjectorColors(minecraft, event.getPoseStack().last().pose(), event.getProjectionMatrix(), projectorCount);
|
||||
} finally {
|
||||
RenderSystem.restoreProjectionMatrix();
|
||||
minecraftExt.setMainRenderTargetOverride(null);
|
||||
minecraft.getMainRenderTarget().bindWrite(true);
|
||||
minecraft.setCameraEntity(player);
|
||||
isRenderingProjectorDepth = false;
|
||||
VISIBLE_PROJECTORS.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [PERF] override any vertex formats to only use pos
|
||||
|
||||
@SubscribeEvent
|
||||
public static void handleRenderGameOverlay(final RenderGameOverlayEvent.Post event) {
|
||||
if (event.getType() != RenderGameOverlayEvent.ElementType.ALL) {
|
||||
return;
|
||||
}
|
||||
|
||||
final PoseStack stack = event.getMatrixStack();
|
||||
stack.pushPose();
|
||||
|
||||
final Tesselator tesselator = Tesselator.getInstance();
|
||||
final BufferBuilder builder = tesselator.getBuilder();
|
||||
|
||||
RenderSystem.setShader(GameRenderer::getPositionTexShader);
|
||||
RenderSystem.setShaderTexture(0, projectorDepthTarget.getDepthTextureId());
|
||||
|
||||
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX);
|
||||
|
||||
builder.vertex(50, 50, 0).uv(0, 0).color(0xFFFF0000).endVertex(); // top left
|
||||
builder.vertex(50, 50 + DEPTH_CAPTURE_SIZE / 2, 0).uv(0, 1).color(0xFF00FF00).endVertex(); // bottom left
|
||||
builder.vertex(50 + DEPTH_CAPTURE_SIZE / 2, 50 + DEPTH_CAPTURE_SIZE / 2, 0).uv(1, 1).color(0xFF0000FF).endVertex(); // bottom right
|
||||
builder.vertex(50 + DEPTH_CAPTURE_SIZE / 2, 50, 0).uv(1, 0).color(0xFFFFFFFF).endVertex(); // top right
|
||||
|
||||
tesselator.end();
|
||||
|
||||
stack.popPose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppresses fog rendering while rendering depth buffer for projectors.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void handleFog(final EntityViewRenderEvent.RenderFogEvent event) {
|
||||
if (isRenderingProjectorDepth) {
|
||||
@@ -132,36 +177,368 @@ public final class ProjectorDepthRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ProjectorDepthCamera extends Camera {
|
||||
public void configure(final Level level, final BlockPos blockPos, final float rotationY) {
|
||||
final ProjectorEntity entity = new ProjectorEntity(level, blockPos, rotationY);
|
||||
setup(level, entity, false, false, 0);
|
||||
/**
|
||||
* Suppresses nameplate rendering while rendering depth buffer for projectors.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void handleNameplate(final RenderNameplateEvent event) {
|
||||
if (isRenderingProjectorDepth) {
|
||||
event.setResult(Event.Result.DENY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates cached rendering info, such as textures holding image data for projectors, to allow expiration.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void handleClientTick(final TickEvent.ClientTickEvent event) {
|
||||
RENDER_INFO.cleanUp();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Stage one of projector rendering, render scene depths from the perspective of all projectors that should
|
||||
* bre rendered. The output is the list of depth buffers, MVP matrices that were used to render them, and the
|
||||
* associated color texture for the projector.
|
||||
*/
|
||||
private static void renderProjectorDepths(final Minecraft minecraft, final ClientLevel level, final Player player,
|
||||
final float partialTicks, final long startNanos,
|
||||
final int projectorCount) {
|
||||
prepareDepthBufferRendering(minecraft);
|
||||
try {
|
||||
final PoseStack viewModelStack = new PoseStack();
|
||||
for (int projectorIndex = 0; projectorIndex < projectorCount; projectorIndex++) {
|
||||
final ProjectorBlockEntity projector = VISIBLE_PROJECTORS.get(projectorIndex);
|
||||
final Direction facing = projector.getBlockState().getValue(ProjectorBlock.FACING);
|
||||
final Vec3 projectorPos = Vec3
|
||||
.atCenterOf(projector.getBlockPos())
|
||||
.add(new Vec3(facing.step()).scale(7 / 16f));
|
||||
|
||||
configureProjectorDepthCamera(level, projectorPos, facing.toYRot());
|
||||
|
||||
RenderSystem.setProjectionMatrix(DEPTH_CAMERA_PROJECTION_MATRIX);
|
||||
setupViewModelMatrix(viewModelStack);
|
||||
|
||||
storeProjectorMatrix(projectorIndex, projectorPos, viewModelStack);
|
||||
|
||||
bindProjectorDepthRenderTarget(projectorIndex, minecraft);
|
||||
|
||||
renderProjectorDepthBuffer(minecraft, partialTicks, startNanos, viewModelStack);
|
||||
|
||||
storeProjectorColorBuffer(projectorIndex, projector);
|
||||
}
|
||||
} finally {
|
||||
finishDepthBufferRendering(minecraft, player);
|
||||
}
|
||||
}
|
||||
|
||||
private static void prepareDepthBufferRendering(final Minecraft minecraft) {
|
||||
isRenderingProjectorDepth = true;
|
||||
|
||||
// Suppresses hit outlines being rendered.
|
||||
hitResultBak = minecraft.hitResult;
|
||||
minecraft.hitResult = null;
|
||||
|
||||
// Skip shadow rendering for perf.
|
||||
entityShadowsBak = minecraft.options.entityShadows;
|
||||
minecraft.options.entityShadows = false;
|
||||
|
||||
minecraft.setCameraEntity(PROJECTOR_DEPTH_CAMERA.getEntity());
|
||||
|
||||
RenderSystem.backupProjectionMatrix();
|
||||
}
|
||||
|
||||
private static void finishDepthBufferRendering(final Minecraft minecraft, final Player player) {
|
||||
minecraft.hitResult = hitResultBak;
|
||||
minecraft.options.entityShadows = entityShadowsBak;
|
||||
|
||||
RenderSystem.restoreProjectionMatrix();
|
||||
|
||||
((MinecraftExt) minecraft).setMainRenderTargetOverride(null);
|
||||
minecraft.getMainRenderTarget().bindWrite(true);
|
||||
|
||||
minecraft.setCameraEntity(player);
|
||||
|
||||
isRenderingProjectorDepth = false;
|
||||
}
|
||||
|
||||
private static void configureProjectorDepthCamera(final ClientLevel level, final Vec3 pos, final float rotationY) {
|
||||
PROJECTOR_DEPTH_CAMERA.setup(level, ProjectorCameraEntity.get(level, pos, rotationY), false, false, 0);
|
||||
}
|
||||
|
||||
private static void setupViewModelMatrix(final PoseStack viewModelStack) {
|
||||
final Quaternion rotation = Vector3f.YP.rotationDegrees(PROJECTOR_DEPTH_CAMERA.getYRot() + 180);
|
||||
viewModelStack.setIdentity();
|
||||
viewModelStack.mulPose(rotation);
|
||||
|
||||
final Matrix3f viewRotationMatrix = viewModelStack.last().normal().copy();
|
||||
if (viewRotationMatrix.invert()) {
|
||||
RenderSystem.setInverseViewRotationMatrix(viewRotationMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
private static void storeProjectorMatrix(final int projectorIndex, final Vec3 projectorPos, final PoseStack viewModelStack) {
|
||||
// Save model-view-projection matrix for mapping in compositing shader. Shifted by position of the
|
||||
// projector, to ensure we use the same origin for projectors and the main camera in compositing.
|
||||
PROJECTOR_CAMERA_MATRICES[projectorIndex].load(DEPTH_CAMERA_PROJECTION_MATRIX);
|
||||
viewModelStack.pushPose();
|
||||
viewModelStack.translate(-projectorPos.x(), -projectorPos.y(), -projectorPos.z());
|
||||
PROJECTOR_CAMERA_MATRICES[projectorIndex].multiply(viewModelStack.last().pose());
|
||||
viewModelStack.popPose();
|
||||
}
|
||||
|
||||
private static void bindProjectorDepthRenderTarget(final int projectorIndex, final Minecraft minecraft) {
|
||||
final DepthOnlyRenderTarget projectorDepthTarget = PROJECTOR_DEPTH_TARGETS[projectorIndex];
|
||||
projectorDepthTarget.bindWrite(true);
|
||||
((MinecraftExt) minecraft).setMainRenderTargetOverride(projectorDepthTarget);
|
||||
}
|
||||
|
||||
private static void renderProjectorDepthBuffer(final Minecraft minecraft, final float partialTicks, final long startNanos, final PoseStack viewModelStack) {
|
||||
final LevelRenderer levelRenderer = minecraft.levelRenderer;
|
||||
levelRenderer.prepareCullFrustum(
|
||||
viewModelStack,
|
||||
PROJECTOR_DEPTH_CAMERA.getPosition(),
|
||||
DEPTH_CAMERA_PROJECTION_MATRIX
|
||||
);
|
||||
levelRenderer.renderLevel(
|
||||
viewModelStack,
|
||||
partialTicks,
|
||||
startNanos,
|
||||
/* shouldRenderBlockOutline: */ false,
|
||||
PROJECTOR_DEPTH_CAMERA,
|
||||
minecraft.gameRenderer,
|
||||
minecraft.gameRenderer.lightTexture(),
|
||||
DEPTH_CAMERA_PROJECTION_MATRIX
|
||||
);
|
||||
}
|
||||
|
||||
private static void storeProjectorColorBuffer(final int projectorIndex, final ProjectorBlockEntity projector) {
|
||||
PROJECTOR_COLOR_TARGETS[projectorIndex] = getRenderInfo(projector).texture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage two or projector rendering, this composes the projections of the projectors being rendered into the
|
||||
* main render target, using the camera matrices and depth information to determine where to render. This is
|
||||
* essentially a post-processing pass, i.e. it renders a screen-filling rectangle blending the projector light
|
||||
* into the existing main render target output.
|
||||
*/
|
||||
private static void renderProjectorColors(final Minecraft minecraft, final Matrix4f modelViewMatrix, final Matrix4f projectionMatrix, final int projectorCount) {
|
||||
prepareColorBufferRendering();
|
||||
|
||||
try {
|
||||
prepareOrthographicRendering(minecraft);
|
||||
|
||||
RenderSystem.setShader(ModShaders::getProjectorsShader);
|
||||
ModShaders.configureProjectorsShader(
|
||||
MAIN_CAMERA_DEPTH,
|
||||
constructInverseMainCameraMatrix(minecraft, modelViewMatrix, projectionMatrix),
|
||||
PROJECTOR_COLOR_TARGETS,
|
||||
PROJECTOR_DEPTH_TARGETS,
|
||||
PROJECTOR_CAMERA_MATRICES,
|
||||
projectorCount
|
||||
);
|
||||
|
||||
renderIntoScreenRect();
|
||||
} finally {
|
||||
finishColorBufferRendering();
|
||||
}
|
||||
}
|
||||
|
||||
private static void prepareColorBufferRendering() {
|
||||
RenderSystem.backupProjectionMatrix();
|
||||
RenderSystem.getModelViewStack().pushPose();
|
||||
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ONE);
|
||||
|
||||
RenderSystem.colorMask(true, true, true, false);
|
||||
RenderSystem.disableDepthTest();
|
||||
RenderSystem.depthMask(false);
|
||||
}
|
||||
|
||||
private static void finishColorBufferRendering() {
|
||||
RenderSystem.depthMask(true);
|
||||
RenderSystem.enableDepthTest();
|
||||
RenderSystem.colorMask(true, true, true, true);
|
||||
RenderSystem.disableBlend();
|
||||
|
||||
RenderSystem.restoreProjectionMatrix();
|
||||
RenderSystem.getModelViewStack().popPose();
|
||||
RenderSystem.applyModelViewMatrix();
|
||||
}
|
||||
|
||||
private static void prepareOrthographicRendering(final Minecraft minecraft) {
|
||||
final Matrix4f screenProjectionMatrix = Matrix4f.orthographic(
|
||||
minecraft.getWindow().getWidth(),
|
||||
-minecraft.getWindow().getHeight(),
|
||||
1000, 3000
|
||||
);
|
||||
RenderSystem.setProjectionMatrix(screenProjectionMatrix);
|
||||
|
||||
final PoseStack modelViewStack = RenderSystem.getModelViewStack();
|
||||
modelViewStack.setIdentity();
|
||||
modelViewStack.translate(0, 0, -2000);
|
||||
RenderSystem.applyModelViewMatrix();
|
||||
}
|
||||
|
||||
private static Matrix4f constructInverseMainCameraMatrix(final Minecraft minecraft, final Matrix4f modelViewMatrix, final Matrix4f projectionMatrix) {
|
||||
final Camera mainCamera = minecraft.gameRenderer.getMainCamera();
|
||||
final Vec3 mainCameraPosition = mainCamera.getPosition();
|
||||
|
||||
final Matrix4f inverseModelViewMatrix = projectionMatrix.copy();
|
||||
inverseModelViewMatrix.multiply(modelViewMatrix);
|
||||
inverseModelViewMatrix.multiplyWithTranslation(-(float) mainCameraPosition.x(), -(float) mainCameraPosition.y(), -(float) mainCameraPosition.z());
|
||||
inverseModelViewMatrix.invert();
|
||||
return inverseModelViewMatrix;
|
||||
}
|
||||
|
||||
private static void renderIntoScreenRect() {
|
||||
final Tesselator tesselator = Tesselator.getInstance();
|
||||
final BufferBuilder builder = tesselator.getBuilder();
|
||||
|
||||
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX);
|
||||
builder.vertex(0, 0, 0).uv(0, 1).endVertex();
|
||||
builder.vertex(0, MAIN_CAMERA_DEPTH.height, 0).uv(0, 0).endVertex();
|
||||
builder.vertex(MAIN_CAMERA_DEPTH.width, MAIN_CAMERA_DEPTH.height, 0).uv(1, 0).endVertex();
|
||||
builder.vertex(MAIN_CAMERA_DEPTH.width, 0, 0).uv(1, 1).endVertex();
|
||||
tesselator.end();
|
||||
}
|
||||
|
||||
private static RenderInfo getRenderInfo(final ProjectorBlockEntity projector) {
|
||||
try {
|
||||
return RENDER_INFO.get(projector, () -> {
|
||||
final DynamicTexture texture = new DynamicTexture(ProjectorVMDevice.WIDTH, ProjectorVMDevice.HEIGHT, false);
|
||||
final RenderInfo renderInfo = new RenderInfo(texture);
|
||||
projector.setFrameConsumer(renderInfo);
|
||||
return renderInfo;
|
||||
});
|
||||
} catch (final ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Tracks a render texture holding color info for rendering projectors.
|
||||
* <p>
|
||||
* Automatically updated by projectors when new data arrives (from a worker thread).
|
||||
*/
|
||||
private record RenderInfo(DynamicTexture texture) implements ProjectorBlockEntity.FrameConsumer {
|
||||
private static final ThreadLocal<byte[]> RGB = ThreadLocal.withInitial(() -> new byte[3]);
|
||||
|
||||
public synchronized void close() {
|
||||
texture.close();
|
||||
}
|
||||
|
||||
private static final class ProjectorEntity extends Player {
|
||||
public ProjectorEntity(final Level level, final BlockPos blockPos, final float rotationY) {
|
||||
super(level, blockPos, rotationY, FakePlayerUtils.getFakePlayerProfile());
|
||||
@Override
|
||||
public synchronized void processFrame(final Picture picture) {
|
||||
final NativeImage image = texture.getPixels();
|
||||
if (image == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getViewYRot(final float partialTicks) {
|
||||
return yRotO;
|
||||
final byte[] y = picture.getPlaneData(0);
|
||||
final byte[] u = picture.getPlaneData(1);
|
||||
final byte[] v = picture.getPlaneData(2);
|
||||
|
||||
// Convert in quads, based on the half resolution of UV. As such, skip every other row, since
|
||||
// we're setting the current and the next.
|
||||
int lumaIndex = 0, chromaIndex = 0;
|
||||
for (int halfRow = 0; halfRow < ProjectorVMDevice.HEIGHT / 2; halfRow++, lumaIndex += ProjectorVMDevice.WIDTH * 2) {
|
||||
final int row = halfRow * 2;
|
||||
for (int halfCol = 0; halfCol < ProjectorVMDevice.WIDTH / 2; halfCol++, chromaIndex++) {
|
||||
final int col = halfCol * 2;
|
||||
final int yIndex = lumaIndex + col;
|
||||
final byte cb = u[chromaIndex];
|
||||
final byte cr = v[chromaIndex];
|
||||
setFromYUV420(image, col, row, y[yIndex], cb, cr);
|
||||
setFromYUV420(image, col + 1, row, y[yIndex + 1], cb, cr);
|
||||
setFromYUV420(image, col, row + 1, y[yIndex + ProjectorVMDevice.WIDTH], cb, cr);
|
||||
setFromYUV420(image, col + 1, row + 1, y[yIndex + ProjectorVMDevice.WIDTH + 1], cb, cr);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getViewXRot(final float partialTicks) {
|
||||
return xRotO;
|
||||
}
|
||||
texture.upload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSpectator() {
|
||||
return false;
|
||||
}
|
||||
private static void setFromYUV420(final NativeImage image, final int col, final int row, final byte y, final byte cb, final byte cr) {
|
||||
final byte[] bytes = RGB.get();
|
||||
Yuv420jToRgb.YUVJtoRGB(y, cb, cr, bytes, 0);
|
||||
final int r = bytes[0] + 128;
|
||||
final int g = bytes[1] + 128;
|
||||
final int b = bytes[2] + 128;
|
||||
image.setPixelRGBA(col, row, r | (g << 8) | (b << 16) | (0xFF << 24));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCreative() {
|
||||
return true;
|
||||
/**
|
||||
* Optimized texture target that doesn't have a color texture, so we can completely skip that when rendering
|
||||
* projector depth buffers.
|
||||
*/
|
||||
private static final class DepthOnlyRenderTarget extends TextureTarget {
|
||||
public DepthOnlyRenderTarget(final int width, final int height) {
|
||||
super(width, height, true, Minecraft.ON_OSX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createBuffers(final int width, final int height, final boolean isOnOSX) {
|
||||
super.createBuffers(width, height, isOnOSX);
|
||||
if (colorTextureId > -1) {
|
||||
if (frameBufferId > -1) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, frameBufferId);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
TextureUtil.releaseTextureId(this.colorTextureId);
|
||||
this.colorTextureId = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake entity used as rendering context when rendering projector depth buffers.
|
||||
*/
|
||||
private static final class ProjectorCameraEntity extends Player {
|
||||
private static ProjectorCameraEntity instance;
|
||||
|
||||
/**
|
||||
* Singleton getter for the fake render entity, to avoid unnecessary allocations.
|
||||
*/
|
||||
public static ProjectorCameraEntity get(final Level level, final Vec3 pos, final float rotationY) {
|
||||
if (instance == null) {
|
||||
instance = new ProjectorCameraEntity(level, BlockPos.ZERO, rotationY);
|
||||
}
|
||||
|
||||
instance.level = level;
|
||||
instance.moveTo(pos.x(), pos.y(), pos.z(), rotationY, 0);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private ProjectorCameraEntity(final Level level, final BlockPos blockPos, final float rotationY) {
|
||||
super(level, blockPos, rotationY, FakePlayerUtils.getFakePlayerProfile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getViewYRot(final float partialTicks) {
|
||||
return yRotO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getViewXRot(final float partialTicks) {
|
||||
return xRotO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSpectator() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCreative() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package li.cil.oc2.client.renderer.blockentity;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.RemovalNotification;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.math.Matrix4f;
|
||||
@@ -12,26 +7,16 @@ import com.mojang.math.Quaternion;
|
||||
import com.mojang.math.Vector3f;
|
||||
import li.cil.oc2.api.API;
|
||||
import li.cil.oc2.client.renderer.ModRenderType;
|
||||
import li.cil.oc2.client.renderer.ProjectorDepthRenderer;
|
||||
import li.cil.oc2.common.block.ProjectorBlock;
|
||||
import li.cil.oc2.common.blockentity.ProjectorBlockEntity;
|
||||
import li.cil.oc2.common.bus.device.vm.ProjectorVMDevice;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.culling.Frustum;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.BitSet;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = API.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
|
||||
public final class ProjectorRenderer implements BlockEntityRenderer<ProjectorBlockEntity> {
|
||||
private static final int LIGHT_COLOR_NEAR = 0x22FFFFFF;
|
||||
@@ -44,11 +29,6 @@ public final class ProjectorRenderer implements BlockEntityRenderer<ProjectorBlo
|
||||
private static final float LENS_BOTTOM = 0 + 4 / 16f;
|
||||
private static final float LENS_TOP = 1 - 4 / 16f;
|
||||
|
||||
private static final Cache<ProjectorBlockEntity, RenderInfo> textures = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(Duration.ofSeconds(5))
|
||||
.removalListener(ProjectorRenderer::handleNoLongerRendering)
|
||||
.build();
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
public ProjectorRenderer(final BlockEntityRendererProvider.Context ignored) {
|
||||
@@ -71,7 +51,7 @@ public final class ProjectorRenderer implements BlockEntityRenderer<ProjectorBlo
|
||||
stack.translate(0.5f, 0, 0.5f);
|
||||
stack.mulPose(rotation);
|
||||
|
||||
renderProjections(projector, stack, bufferSource);
|
||||
ProjectorDepthRenderer.addProjector(projector);
|
||||
|
||||
renderProjectorLight(stack, bufferSource);
|
||||
|
||||
@@ -80,70 +60,6 @@ public final class ProjectorRenderer implements BlockEntityRenderer<ProjectorBlo
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
private void renderProjections(final ProjectorBlockEntity projector, final PoseStack stack, final MultiBufferSource bufferSource) {
|
||||
final ProjectorBlockEntity.VisibilityData visibilityData = projector.getVisibilityData();
|
||||
final BitSet[] visibilities = visibilityData.visibilities();
|
||||
if (hasNoVisibleTiles(visibilities)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final BlockPos projectorPos = projector.getBlockPos();
|
||||
final Frustum frustum = new Frustum(stack.last().pose(), RenderSystem.getProjectionMatrix());
|
||||
frustum.prepare(projectorPos.getX(), projectorPos.getY(), projectorPos.getZ());
|
||||
if (!frustum.isVisible(visibilityData.visibilityBounds())) {
|
||||
return;
|
||||
}
|
||||
|
||||
stack.pushPose();
|
||||
|
||||
stack.translate(0, 0, 0.49);
|
||||
|
||||
final RenderType renderType = getUpdatedRenderType(projector);
|
||||
final VertexConsumer consumer = bufferSource.getBuffer(renderType);
|
||||
for (int distance = 0; distance < visibilities.length; distance++) {
|
||||
final BitSet visibility = visibilities[distance];
|
||||
if (!visibility.isEmpty()) {
|
||||
stack.pushPose();
|
||||
|
||||
stack.translate(0, 0, distance + 1);
|
||||
|
||||
renderProjection(stack, consumer, ProjectorBlockEntity.getLayerSize(distance), visibility);
|
||||
|
||||
stack.popPose();
|
||||
}
|
||||
}
|
||||
|
||||
stack.popPose();
|
||||
}
|
||||
|
||||
private void renderProjection(final PoseStack stack, final VertexConsumer consumer, final ProjectorBlockEntity.LayerSize layerSize, final BitSet visibility) {
|
||||
final float width = layerSize.width();
|
||||
final float height = layerSize.height();
|
||||
final int discreteWidth = layerSize.discreteWidth();
|
||||
final int discreteHeight = layerSize.discreteHeight();
|
||||
final float uOffset = (discreteHeight / height - 1) / 2f;
|
||||
final float vOffset = (discreteWidth / width - 1) / 2f;
|
||||
|
||||
stack.translate(-width / 2.0, (discreteHeight - height) / 2.0, 0);
|
||||
stack.scale(width, height, 1);
|
||||
final Matrix4f matrix = stack.last().pose();
|
||||
|
||||
for (int index = visibility.nextSetBit(0); index >= 0; index = visibility.nextSetBit(index + 1)) {
|
||||
final int x = index % discreteWidth;
|
||||
final int y = index / discreteWidth;
|
||||
|
||||
final float u0 = Math.max(0, x / width - vOffset);
|
||||
final float u1 = Math.min(1, (x + 1) / width - vOffset);
|
||||
final float v0 = Math.max(0, y / height - uOffset);
|
||||
final float v1 = Math.min(1, (y + 1) / height - uOffset);
|
||||
|
||||
consumer.vertex(matrix, u0, v0, 0).uv(1 - u0, 1 - v0).endVertex();
|
||||
consumer.vertex(matrix, u1, v0, 0).uv(1 - u1, 1 - v0).endVertex();
|
||||
consumer.vertex(matrix, u1, v1, 0).uv(1 - u1, 1 - v1).endVertex();
|
||||
consumer.vertex(matrix, u0, v1, 0).uv(1 - u0, 1 - v1).endVertex();
|
||||
}
|
||||
}
|
||||
|
||||
private void renderProjectorLight(final PoseStack stack, final MultiBufferSource bufferSource) {
|
||||
stack.pushPose();
|
||||
|
||||
@@ -206,54 +122,4 @@ public final class ProjectorRenderer implements BlockEntityRenderer<ProjectorBlo
|
||||
consumer.vertex(matrix, ledLeft, ledTop, ledDepth).color(LED_COLOR).endVertex();
|
||||
consumer.vertex(matrix, ledRight, ledTop, ledDepth).color(LED_COLOR).endVertex();
|
||||
}
|
||||
|
||||
private static boolean hasNoVisibleTiles(final BitSet[] visibilities) {
|
||||
for (final BitSet visibility : visibilities) {
|
||||
if (!visibility.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static RenderType getUpdatedRenderType(final ProjectorBlockEntity projector) {
|
||||
final RenderInfo renderInfo = getRenderInfo(projector);
|
||||
|
||||
final NativeImage image = renderInfo.texture().getPixels();
|
||||
assert image != null;
|
||||
if (projector.updateRenderTexture(image::setPixelRGBA)) {
|
||||
renderInfo.texture().upload();
|
||||
}
|
||||
|
||||
return renderInfo.renderType();
|
||||
}
|
||||
|
||||
private static RenderInfo getRenderInfo(final ProjectorBlockEntity projector) {
|
||||
try {
|
||||
return ProjectorRenderer.textures.get(projector, () -> {
|
||||
final DynamicTexture texture = new DynamicTexture(
|
||||
ProjectorVMDevice.WIDTH,
|
||||
ProjectorVMDevice.HEIGHT,
|
||||
false
|
||||
);
|
||||
return new RenderInfo(texture, ModRenderType.getProjector(texture));
|
||||
}
|
||||
);
|
||||
} catch (final ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void updateCache(final TickEvent.ClientTickEvent event) {
|
||||
textures.cleanUp();
|
||||
}
|
||||
|
||||
private static void handleNoLongerRendering(final RemovalNotification<ProjectorBlockEntity, RenderInfo> notification) {
|
||||
final RenderInfo renderInfo = notification.getValue();
|
||||
assert renderInfo != null;
|
||||
renderInfo.texture().close();
|
||||
}
|
||||
|
||||
private record RenderInfo(DynamicTexture texture, RenderType renderType) { }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package li.cil.oc2.common.block;
|
||||
|
||||
import li.cil.oc2.common.blockentity.BlockEntities;
|
||||
import li.cil.oc2.common.blockentity.ProjectorBlockEntity;
|
||||
import li.cil.oc2.common.util.BlockEntityUtils;
|
||||
import li.cil.oc2.common.blockentity.TickableBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
@@ -40,11 +39,7 @@ public final class ProjectorBlock extends HorizontalDirectionalBlock implements
|
||||
@Nullable
|
||||
@Override
|
||||
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(final Level level, final BlockState state, final BlockEntityType<T> type) {
|
||||
if (!level.isClientSide()) {
|
||||
return BlockEntityUtils.createTicker(type, BlockEntities.PROJECTOR.get(), ProjectorBlockEntity::serverTick);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return TickableBlockEntity.createServerTicker(level, type, BlockEntities.PROJECTOR.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,23 +13,18 @@ import li.cil.oc2.jcodec.codecs.h264.H264Encoder;
|
||||
import li.cil.oc2.jcodec.codecs.h264.encode.CQPRateControl;
|
||||
import li.cil.oc2.jcodec.common.model.ColorSpace;
|
||||
import li.cil.oc2.jcodec.common.model.Picture;
|
||||
import li.cil.oc2.jcodec.scale.Yuv420jToRgb;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.util.profiling.ProfilerFiller;
|
||||
import net.minecraft.world.level.ClipContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -41,18 +36,19 @@ import java.util.zip.Inflater;
|
||||
|
||||
// TODO Only send frames to watching clients (have clients send "keepalive" packets when rendering this).
|
||||
// TODO Throttle update speed by distance to closest player and max number any player watching this projector is watching in total.
|
||||
public final class ProjectorBlockEntity extends ModBlockEntity {
|
||||
public final class ProjectorBlockEntity extends ModBlockEntity implements TickableBlockEntity {
|
||||
@FunctionalInterface
|
||||
public interface FramebufferPixelSetter {
|
||||
void set(final int x, final int y, final int rgba);
|
||||
public interface FrameConsumer {
|
||||
void processFrame(final Picture picture);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
|
||||
private static final int MAX_RENDER_DISTANCE = 12;
|
||||
private static final int MAX_WIDTH = MAX_RENDER_DISTANCE + 1; // +1 To make it odd, so we can center.
|
||||
private static final int MAX_HEIGHT = (MAX_RENDER_DISTANCE * ProjectorVMDevice.HEIGHT / ProjectorVMDevice.WIDTH) + 1; // + 1 To match horizontal margin.
|
||||
private static final LayerSize[] LAYER_SIZES = computeLayerSizes();
|
||||
|
||||
private static final int FRAME_EVERY_N_TICKS = 5;
|
||||
|
||||
@@ -74,7 +70,7 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
|
||||
|
||||
// Video transfer.
|
||||
private final H264Encoder encoder = new H264Encoder(new CQPRateControl(12));
|
||||
private Future<?> runningEncode; // To allow waiting for previous frame to finish.
|
||||
@Nullable private Future<?> runningEncode; // To allow waiting for previous frame to finish.
|
||||
private final Picture picture = Picture.create(ProjectorVMDevice.WIDTH, ProjectorVMDevice.HEIGHT, ColorSpace.YUV420J);
|
||||
|
||||
private boolean needsIDR = true; // Whether we need to send a keyframe next.
|
||||
@@ -83,12 +79,10 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
|
||||
|
||||
// Client only data.
|
||||
private final H264Decoder decoder = new H264Decoder();
|
||||
private Future<?> runningDecode; // Current decoding operation, if any, to avoid race conditions.
|
||||
@Nullable private Future<?> runningDecode; // Current decoding operation, if any, to avoid race conditions.
|
||||
private final ByteBuffer decoderBuffer = ByteBuffer.allocateDirect(1024 * 1024); // Re-used decompression buffer.
|
||||
private volatile boolean isBufferDirty; // Whether buffer has changed and renderers need to update their texture.
|
||||
@Nullable private FrameConsumer frameConsumer; // Where to throw received frames.
|
||||
|
||||
private final BitSet[] visibilities = new BitSet[MAX_RENDER_DISTANCE]; // Index of blocks we're projecting onto.
|
||||
private AABB visibilityBounds; // Bounds of all blocks we're projecting onto.
|
||||
private AABB renderBounds; // Overall render bounds, disregarding projection surface, to allow growing if necessary.
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
@@ -98,20 +92,11 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
|
||||
|
||||
encoder.setKeyInterval(100);
|
||||
|
||||
for (int i = 0; i < visibilities.length; i++) {
|
||||
visibilities[i] = new BitSet(MAX_WIDTH * MAX_HEIGHT);
|
||||
}
|
||||
visibilityBounds = super.getRenderBoundingBox();
|
||||
|
||||
updateRenderBounds();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
|
||||
public static void serverTick(final Level ignoredLevel, final BlockPos ignoredPos, final BlockState ignoredState, final ProjectorBlockEntity projector) {
|
||||
projector.serverTick();
|
||||
}
|
||||
|
||||
public ProjectorVMDevice getProjectorDevice() {
|
||||
return projectorDevice;
|
||||
}
|
||||
@@ -128,12 +113,86 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
|
||||
Arrays.fill(picture.getPlaneData(1), (byte) 0);
|
||||
Arrays.fill(picture.getPlaneData(2), (byte) 0);
|
||||
needsIDR = true;
|
||||
isBufferDirty = true;
|
||||
}
|
||||
|
||||
sendRunningState();
|
||||
}
|
||||
|
||||
public void setFrameConsumer(@Nullable final FrameConsumer consumer) {
|
||||
if (consumer == frameConsumer) {
|
||||
return;
|
||||
}
|
||||
synchronized (picture) {
|
||||
this.frameConsumer = consumer;
|
||||
if (frameConsumer != null) {
|
||||
frameConsumer.processFrame(picture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serverTick() {
|
||||
if (!isProjecting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (energy.extractEnergy(Config.projectorEnergyPerTick, true) < Config.projectorEnergyPerTick) {
|
||||
if (hasEnergy) {
|
||||
hasEnergy = false;
|
||||
sendRunningState();
|
||||
}
|
||||
return;
|
||||
} else if (!hasEnergy) {
|
||||
hasEnergy = true;
|
||||
sendRunningState();
|
||||
}
|
||||
|
||||
sendBudget.updateAndGet(budget -> Math.min(Config.projectorMaxBytesPerTick * 10, budget + Config.projectorMaxBytesPerTick));
|
||||
nextFrameIn = Math.max(0, nextFrameIn - 1);
|
||||
if (sendBudget.get() < 0 || nextFrameIn > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (runningEncode != null && !runningEncode.isDone()) {
|
||||
return;
|
||||
}
|
||||
|
||||
joinWorkerAndLogErrors(runningEncode);
|
||||
|
||||
nextFrameIn = FRAME_EVERY_N_TICKS;
|
||||
|
||||
if (level == null || !(level.getChunk(getBlockPos()) instanceof LevelChunk chunk)) {
|
||||
return;
|
||||
}
|
||||
|
||||
runningEncode = FRAME_WORKERS.submit(() -> {
|
||||
final boolean hasChanges = projectorDevice.applyChanges(picture);
|
||||
if (!hasChanges && !needsIDR) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ByteBuffer frameData;
|
||||
if (needsIDR) {
|
||||
frameData = encoder.encodeIDRFrame(picture, ByteBuffer.allocateDirect(256 * 1024));
|
||||
needsIDR = false;
|
||||
} else {
|
||||
frameData = encoder.encodeFrame(picture, ByteBuffer.allocateDirect(256 * 1024)).data();
|
||||
}
|
||||
|
||||
final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
|
||||
deflater.setInput(frameData);
|
||||
deflater.finish();
|
||||
final ByteBuffer compressedFrameData = ByteBuffer.allocateDirect(1024 * 1024);
|
||||
deflater.deflate(compressedFrameData, Deflater.FULL_FLUSH);
|
||||
deflater.end();
|
||||
compressedFrameData.flip();
|
||||
|
||||
sendBudget.accumulateAndGet(compressedFrameData.limit(), (budget, packetSize) -> budget - packetSize);
|
||||
final ProjectorFramebufferMessage message = new ProjectorFramebufferMessage(this, compressedFrameData);
|
||||
Network.sendToClientsTrackingChunk(message, chunk);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag getUpdateTag() {
|
||||
final CompoundTag tag = super.getUpdateTag();
|
||||
@@ -178,64 +237,11 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
|
||||
updateRenderBounds();
|
||||
}
|
||||
|
||||
public record LayerSize(float width, float height, int discreteWidth, int discreteHeight) { }
|
||||
|
||||
public static LayerSize getLayerSize(final int distance) {
|
||||
return LAYER_SIZES[distance];
|
||||
}
|
||||
|
||||
public record VisibilityData(AABB visibilityBounds, BitSet[] visibilities) { }
|
||||
|
||||
public VisibilityData getVisibilityData() {
|
||||
updateVisibilities();
|
||||
return new VisibilityData(visibilityBounds, visibilities);
|
||||
}
|
||||
|
||||
public boolean updateRenderTexture(final FramebufferPixelSetter setter) {
|
||||
assert level != null;
|
||||
final ProfilerFiller profiler = level.getProfiler();
|
||||
profiler.push("updateRenderTexture");
|
||||
|
||||
if (!isBufferDirty) {
|
||||
profiler.pop();
|
||||
return false;
|
||||
}
|
||||
|
||||
isBufferDirty = false;
|
||||
|
||||
final byte[] y = picture.getPlaneData(0);
|
||||
final byte[] u = picture.getPlaneData(1);
|
||||
final byte[] v = picture.getPlaneData(2);
|
||||
|
||||
// Convert in quads, based on the half resolution of UV. As such, skip every other row, since
|
||||
// we're setting the current and the next.
|
||||
int lumaIndex = 0, chromaIndex = 0;
|
||||
for (int halfRow = 0; halfRow < ProjectorVMDevice.HEIGHT / 2; halfRow++, lumaIndex += ProjectorVMDevice.WIDTH * 2) {
|
||||
final int row = halfRow * 2;
|
||||
for (int halfCol = 0; halfCol < ProjectorVMDevice.WIDTH / 2; halfCol++, chromaIndex++) {
|
||||
final int col = halfCol * 2;
|
||||
final int yIndex = lumaIndex + col;
|
||||
final byte cb = u[chromaIndex];
|
||||
final byte cr = v[chromaIndex];
|
||||
setFromYUV420(setter, col, row, y[yIndex], cb, cr);
|
||||
setFromYUV420(setter, col + 1, row, y[yIndex + 1], cb, cr);
|
||||
setFromYUV420(setter, col, row + 1, y[yIndex + ProjectorVMDevice.WIDTH], cb, cr);
|
||||
setFromYUV420(setter, col + 1, row + 1, y[yIndex + ProjectorVMDevice.WIDTH + 1], cb, cr);
|
||||
}
|
||||
}
|
||||
|
||||
profiler.pop();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void applyNextFrame(final ByteBuffer frameData) {
|
||||
final Future<?> lastDecode = runningDecode;
|
||||
runningDecode = FRAME_WORKERS.submit(() -> {
|
||||
try {
|
||||
if (lastDecode != null) {
|
||||
lastDecode.get();
|
||||
}
|
||||
joinWorkerAndLogErrors(lastDecode);
|
||||
|
||||
final Inflater inflater = new Inflater();
|
||||
inflater.setInput(frameData);
|
||||
@@ -246,8 +252,12 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
|
||||
|
||||
decoder.decodeFrame(decoderBuffer, picture.getData());
|
||||
|
||||
isBufferDirty = true;
|
||||
} catch (final InterruptedException | ExecutionException | DataFormatException ignored) {
|
||||
synchronized (picture) {
|
||||
if (frameConsumer != null) {
|
||||
frameConsumer.processFrame(picture);
|
||||
}
|
||||
}
|
||||
} catch (final DataFormatException ignored) {
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -267,62 +277,6 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
|
||||
return Config.projectorEnergyStorage > 0 && Config.projectorEnergyPerTick > 0;
|
||||
}
|
||||
|
||||
private void serverTick() {
|
||||
if (!isProjecting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (energy.extractEnergy(Config.projectorEnergyPerTick, true) < Config.projectorEnergyPerTick) {
|
||||
if (hasEnergy) {
|
||||
hasEnergy = false;
|
||||
sendRunningState();
|
||||
}
|
||||
return;
|
||||
} else if (!hasEnergy) {
|
||||
hasEnergy = true;
|
||||
sendRunningState();
|
||||
}
|
||||
|
||||
sendBudget.updateAndGet(budget -> Math.min(Config.projectorMaxBytesPerTick * 10, budget + Config.projectorMaxBytesPerTick));
|
||||
nextFrameIn = Math.max(0, nextFrameIn - 1);
|
||||
if (sendBudget.get() < 0 || nextFrameIn > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (runningEncode != null && !runningEncode.isDone()) {
|
||||
return;
|
||||
}
|
||||
|
||||
nextFrameIn = FRAME_EVERY_N_TICKS;
|
||||
|
||||
runningEncode = FRAME_WORKERS.submit(() -> {
|
||||
final boolean hasChanges = projectorDevice.applyChanges(picture);
|
||||
if (!hasChanges && !needsIDR) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ByteBuffer frameData;
|
||||
if (needsIDR) {
|
||||
frameData = encoder.encodeIDRFrame(picture, ByteBuffer.allocateDirect(256 * 1024));
|
||||
needsIDR = false;
|
||||
} else {
|
||||
frameData = encoder.encodeFrame(picture, ByteBuffer.allocateDirect(256 * 1024)).data();
|
||||
}
|
||||
|
||||
final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
|
||||
deflater.setInput(frameData);
|
||||
deflater.finish();
|
||||
final ByteBuffer compressedFrameData = ByteBuffer.allocateDirect(1024 * 1024);
|
||||
deflater.deflate(compressedFrameData, Deflater.FULL_FLUSH);
|
||||
deflater.end();
|
||||
compressedFrameData.flip();
|
||||
|
||||
sendBudget.accumulateAndGet(compressedFrameData.limit(), (budget, packetSize) -> budget - packetSize);
|
||||
final ProjectorFramebufferMessage message = new ProjectorFramebufferMessage(this, compressedFrameData);
|
||||
Network.sendToClientsTrackingBlockEntity(message, this);
|
||||
});
|
||||
}
|
||||
|
||||
private void sendRunningState() {
|
||||
if (level != null && !level.isClientSide()) {
|
||||
Network.sendToClientsTrackingBlockEntity(new ProjectorStateMessage(this, isProjecting && hasEnergy), this);
|
||||
@@ -337,118 +291,23 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
|
||||
final BlockPos projectorPos = getBlockPos();
|
||||
final BlockPos screenBasePos = projectorPos.relative(blockFacing, MAX_RENDER_DISTANCE);
|
||||
final BlockPos screenMinPos = screenBasePos.relative(screenLeft.getOpposite(), MAX_WIDTH / 2);
|
||||
final BlockPos screenMaxPos = screenBasePos
|
||||
.relative(screenLeft, MAX_WIDTH / 2)
|
||||
final BlockPos screenMaxPos = screenBasePos.relative(screenLeft, MAX_WIDTH / 2)
|
||||
// -1 for the MAX_HEIGHT padding, -1 for auto-expansion of AABB constructor
|
||||
.relative(screenUp, MAX_HEIGHT - 2);
|
||||
|
||||
renderBounds = new AABB(getBlockPos()).minmax(new AABB(screenMinPos)).minmax(new AABB(screenMaxPos));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild "stencil buffer" of blocks hit in projection direction, per block layer, up to max distance.
|
||||
*/
|
||||
private void updateVisibilities() {
|
||||
final Direction blockFacing = getBlockState().getValue(ProjectorBlock.FACING);
|
||||
final Direction screenUp = Direction.UP;
|
||||
final Direction screenLeft = blockFacing.getCounterClockWise();
|
||||
|
||||
final BlockPos projectorPos = getBlockPos();
|
||||
final BlockPos screenBasePos = projectorPos.relative(blockFacing, MAX_RENDER_DISTANCE + 1);
|
||||
final BlockPos screenOriginPos = screenBasePos.relative(screenLeft.getOpposite(), MAX_WIDTH / 2);
|
||||
|
||||
final Vec3 toFaceCenter = new Vec3(blockFacing.getOpposite().step()).scale(0.45);
|
||||
final Vec3 clipStartPos = Vec3.atCenterOf(projectorPos.relative(blockFacing)).subtract(toFaceCenter);
|
||||
final Vec3 stepOrigin = Vec3.atCenterOf(screenOriginPos).add(toFaceCenter);
|
||||
final Vec3 upStep = new Vec3(screenUp.step());
|
||||
final Vec3 leftStep = new Vec3(screenLeft.step());
|
||||
|
||||
for (final BitSet bitSet : visibilities) {
|
||||
bitSet.clear();
|
||||
}
|
||||
|
||||
AABB bounds = new AABB(getBlockPos()).minmax(new AABB(getBlockPos().relative(blockFacing)));
|
||||
|
||||
final Level level = getLevel();
|
||||
if (level == null) {
|
||||
private static void joinWorkerAndLogErrors(@Nullable final Future<?> job) {
|
||||
if (job == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int y = 0; y < MAX_HEIGHT + 1; y++) {
|
||||
for (int x = 0; x < MAX_WIDTH; x++) {
|
||||
final Vec3 clipEndPos = stepOrigin.add(upStep.scale(y)).add(leftStep.scale(x));
|
||||
final ClipContext context = new ClipContext(clipStartPos, clipEndPos, ClipContext.Block.VISUAL, ClipContext.Fluid.NONE, null);
|
||||
final BlockHitResult hit = level.clip(context);
|
||||
if (hit.getType() == HitResult.Type.MISS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hit.getDirection() != blockFacing.getOpposite()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final BlockState blockState = level.getBlockState(hit.getBlockPos());
|
||||
if (!blockState.isFaceSturdy(level, hit.getBlockPos(), blockFacing.getOpposite())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final BlockPos delta = hit.getBlockPos().subtract(projectorPos);
|
||||
final int distance = Math.abs(delta.get(blockFacing.getAxis())) - 2;
|
||||
if (distance >= visibilities.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final int globalX = delta.get(screenLeft.getAxis());
|
||||
final int globalY = delta.get(screenUp.getAxis());
|
||||
|
||||
final ProjectorBlockEntity.LayerSize layerSize = getLayerSize(distance);
|
||||
final int discreteWidth = layerSize.discreteWidth();
|
||||
final int discreteHeight = layerSize.discreteHeight();
|
||||
|
||||
if (globalY < 0 || globalY >= discreteHeight) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final int localX = globalX + discreteWidth / 2;
|
||||
if (localX < 0 || localX >= discreteWidth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bounds = bounds.minmax(new AABB(hit.getBlockPos()));
|
||||
|
||||
final int index = localX + globalY * discreteWidth;
|
||||
visibilities[distance].set(index);
|
||||
}
|
||||
try {
|
||||
job.get();
|
||||
} catch (final InterruptedException ignored) {
|
||||
} catch (final ExecutionException e) {
|
||||
LOGGER.error(e);
|
||||
}
|
||||
|
||||
visibilityBounds = bounds;
|
||||
}
|
||||
|
||||
private static final ThreadLocal<byte[]> rgb = ThreadLocal.withInitial(() -> new byte[3]);
|
||||
|
||||
private static void setFromYUV420(final FramebufferPixelSetter setter, final int col, final int row, final byte y, final byte cb, final byte cr) {
|
||||
final byte[] bytes = rgb.get();
|
||||
Yuv420jToRgb.YUVJtoRGB(y, cb, cr, bytes, 0);
|
||||
final int r = bytes[0] + 128;
|
||||
final int g = bytes[1] + 128;
|
||||
final int b = bytes[2] + 128;
|
||||
setter.set(col, row, r | (g << 8) | (b << 16) | (0xFF << 24));
|
||||
}
|
||||
|
||||
private static LayerSize[] computeLayerSizes() {
|
||||
final LayerSize[] layerSizes = new LayerSize[MAX_RENDER_DISTANCE];
|
||||
for (int distance = 0; distance < layerSizes.length; distance++) {
|
||||
final float bufferWidth = ProjectorVMDevice.WIDTH;
|
||||
final float bufferHeight = ProjectorVMDevice.HEIGHT;
|
||||
final float ratio = bufferHeight / bufferWidth;
|
||||
final float width = (MAX_WIDTH - 1) * (float) (distance + 1) / MAX_RENDER_DISTANCE;
|
||||
final float height = width * ratio;
|
||||
int discreteWidth = (int) Math.ceil(width);
|
||||
discreteWidth += 1 - (discreteWidth & 1); // we center, so even values eat up one more
|
||||
int discreteHeight = (int) Math.ceil(height); // we align, so actual height is correct
|
||||
if (Math.abs(discreteHeight - height) < 0.001) discreteHeight++;
|
||||
layerSizes[distance] = new LayerSize(width, height, discreteWidth, discreteHeight);
|
||||
}
|
||||
return layerSizes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,120 @@
|
||||
package li.cil.oc2.common.mixin;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Matrix4f;
|
||||
import li.cil.oc2.client.renderer.ProjectorDepthRenderer;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.LevelRenderer;
|
||||
import net.minecraft.client.renderer.*;
|
||||
import net.minecraft.client.renderer.culling.Frustum;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@Mixin(LevelRenderer.class)
|
||||
public abstract class LevelRendererMixin {
|
||||
@Inject(method = {"shouldShowEntityOutlines"}, at = @At("HEAD"), cancellable = true)
|
||||
@Shadow
|
||||
@Final
|
||||
private RenderBuffers renderBuffers;
|
||||
@Shadow
|
||||
@Final
|
||||
private Minecraft minecraft;
|
||||
@Shadow
|
||||
private Frustum cullingFrustum;
|
||||
|
||||
@Shadow
|
||||
@Nullable
|
||||
private RenderTarget itemEntityTarget;
|
||||
@Nullable
|
||||
private RenderTarget itemEntityTargetBak;
|
||||
|
||||
@Shadow
|
||||
@Nullable
|
||||
private RenderTarget weatherTarget;
|
||||
@Nullable
|
||||
private RenderTarget weatherTargetBak;
|
||||
|
||||
|
||||
@Inject(method = "renderLevel", at = @At("HEAD"))
|
||||
private void prepareDepthRendering(final CallbackInfo ci) {
|
||||
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
|
||||
itemEntityTargetBak = itemEntityTarget;
|
||||
itemEntityTarget = null;
|
||||
weatherTargetBak = weatherTarget;
|
||||
weatherTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "renderLevel", at = @At("TAIL"))
|
||||
private void cleanupDepthRendering(final CallbackInfo ci) {
|
||||
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
|
||||
cleanupDepthRendering();
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupDepthRendering() {
|
||||
weatherTarget = weatherTargetBak;
|
||||
itemEntityTarget = itemEntityTargetBak;
|
||||
}
|
||||
|
||||
@Inject(method = "renderLevel", at = @At(value = "INVOKE_STRING", target = "Lnet/minecraft/util/profiling/ProfilerFiller;popPush(Ljava/lang/String;)V", args = {"ldc=destroyProgress"}), cancellable = true)
|
||||
private void captureDepthAndEarlyExit(
|
||||
final PoseStack stack,
|
||||
final float partialTicks,
|
||||
final long startNanos,
|
||||
final boolean shouldRenderBlockOutline,
|
||||
final Camera camera,
|
||||
final GameRenderer gameRenderer,
|
||||
final LightTexture lightTexture,
|
||||
final Matrix4f projectionMatrix,
|
||||
final CallbackInfo ci
|
||||
) {
|
||||
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
|
||||
// If we're rendering depth, we can skip most of the rest here: we don't need destruction progress,
|
||||
// transparency, hit result, debug stuff, clouds or weather.
|
||||
cleanupDepthRendering();
|
||||
|
||||
// We do want particles though, because that's a neat effect.
|
||||
final MultiBufferSource.BufferSource bufferSource = renderBuffers.bufferSource();
|
||||
minecraft.particleEngine.render(stack, bufferSource, lightTexture, camera, partialTicks, cullingFrustum);
|
||||
|
||||
// Clean up anything regular return would also clean up.
|
||||
bufferSource.endBatch();
|
||||
RenderSystem.depthMask(true);
|
||||
RenderSystem.disableBlend();
|
||||
RenderSystem.applyModelViewMatrix();
|
||||
FogRenderer.setupNoFog();
|
||||
ci.cancel();
|
||||
} else {
|
||||
// Otherwise, we grab the depth buffer of the main render target here, before
|
||||
// fabulous shading breaks it.
|
||||
ProjectorDepthRenderer.captureMainCameraDepth();
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "shouldShowEntityOutlines", at = @At("HEAD"), cancellable = true)
|
||||
private void skipOutlines(final CallbackInfoReturnable<Boolean> cir) {
|
||||
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
|
||||
cir.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = {"renderSky", "renderClouds", "renderDebug"}, at = @At("HEAD"), cancellable = true)
|
||||
private void skipExtraStuff(final CallbackInfo ci) {
|
||||
@Inject(method = "renderSky", at = @At("HEAD"), cancellable = true)
|
||||
private void skipSky(final CallbackInfo ci) {
|
||||
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = {"entityTarget", "getItemEntityTarget", "getParticlesTarget"}, at = @At("HEAD"), cancellable = true)
|
||||
@Inject(method = {"entityTarget", "getItemEntityTarget"}, at = @At("HEAD"), cancellable = true)
|
||||
private void redirectToMainTarget(final CallbackInfoReturnable<RenderTarget> cir) {
|
||||
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
|
||||
cir.setReturnValue(Minecraft.getInstance().getMainRenderTarget());
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package li.cil.oc2.common.mixin;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import li.cil.oc2.client.renderer.ProjectorDepthRenderer;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
import net.minecraft.client.renderer.ShaderInstance;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(RenderSystem.class)
|
||||
public abstract class RenderSystemMixin {
|
||||
@Inject(method = "getShader", at = @At("HEAD"), cancellable = true)
|
||||
private static void overrideShader(final CallbackInfoReturnable<ShaderInstance> cir) {
|
||||
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
|
||||
cir.setReturnValue(GameRenderer.getPositionShader());
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/main/resources/assets/oc2/shaders/core/projectors.fsh
Normal file
139
src/main/resources/assets/oc2/shaders/core/projectors.fsh
Normal file
@@ -0,0 +1,139 @@
|
||||
#version 150
|
||||
|
||||
uniform int Count;
|
||||
|
||||
// Projector 1
|
||||
uniform sampler2D Sampler0; // Main Depth
|
||||
uniform mat4 InverseMainCamera; // Inverse Main View Projection Matrix
|
||||
|
||||
// Projector 2
|
||||
uniform sampler2D Sampler1; // Color
|
||||
uniform sampler2D Sampler2; // Depth
|
||||
uniform mat4 ProjectorCamera0;
|
||||
|
||||
// Projector 3
|
||||
uniform sampler2D Sampler3; // Color
|
||||
uniform sampler2D Sampler4; // Depth
|
||||
uniform mat4 ProjectorCamera1;
|
||||
|
||||
// Projector 4
|
||||
uniform sampler2D Sampler5; // Color
|
||||
uniform sampler2D Sampler6; // Depth
|
||||
uniform mat4 ProjectorCamera2;
|
||||
|
||||
const float PROJECTOR_NEAR = 1.0/16.0;
|
||||
const float PROJECTOR_FAR = 32.0;
|
||||
const float MAX_DISTANCE = 16;
|
||||
const float START_FADE_DISTANCE = 12;
|
||||
const float DOT_EPSILON = 0.25;
|
||||
const mat4 CLIP_TO_TEX = mat4(
|
||||
0.5, 0, 0, 0,
|
||||
0, 0.5, 0, 0,
|
||||
0, 0, 0.5, 0,
|
||||
0.5, 0.5, 0.5, 1
|
||||
);
|
||||
|
||||
in vec2 texCoord;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
vec3 getWorldPos(vec2 uv, float clipDepth) {
|
||||
vec2 clipUv = uv * 2 - 1;
|
||||
vec4 clipPos = vec4(clipUv, clipDepth, 1);
|
||||
vec4 worldPos = InverseMainCamera * clipPos;
|
||||
return worldPos.xyz / worldPos.w;
|
||||
}
|
||||
|
||||
vec3 getWorldPos(vec2 uv) {
|
||||
float depth = texture2D(Sampler1, uv).r;
|
||||
float clipDepth = depth * 2 - 1;
|
||||
return getWorldPos(uv, clipDepth);
|
||||
}
|
||||
|
||||
vec3 getNormal(vec3 worldPos) {
|
||||
return normalize(cross(dFdx(worldPos), dFdy(worldPos)));
|
||||
}
|
||||
|
||||
bool isInClipBounds(vec3 v) {
|
||||
return v.x >= -1 && v.x <= 1 &&
|
||||
v.y >= -1 && v.y <= 1 &&
|
||||
v.z >= -1 && v.z <= 1;
|
||||
}
|
||||
|
||||
float toLinearDepth(float depth, float zNear, float zFar) {
|
||||
return zNear * zFar / (zFar + depth * (zNear - zFar));
|
||||
}
|
||||
|
||||
bool getProjectorColor(vec3 worldPos, vec3 worldNormal, mat4 projectorCamera,
|
||||
sampler2D projectorColorSampler, sampler2D projectorDepthSampler,
|
||||
out vec4 result) {
|
||||
// Project world normal into projector clip space.
|
||||
vec3 projectorClipNormal = (projectorCamera * vec4(worldNormal, 0)).xyz;
|
||||
|
||||
// Cull sides and back-faces.
|
||||
float d = dot(projectorClipNormal, vec3(0, 0, -1));
|
||||
if (d <= DOT_EPSILON) {
|
||||
return false;
|
||||
}
|
||||
|
||||
vec4 projectorClipPosPrediv = projectorCamera * vec4(worldPos, 1);
|
||||
float linearDepth = projectorClipPosPrediv.z;
|
||||
if (linearDepth >= MAX_DISTANCE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
vec3 projectorClipPos = projectorClipPosPrediv.xyz / projectorClipPosPrediv.w;
|
||||
if (!isInClipBounds(projectorClipPos)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
vec4 projectorUvPrediv = CLIP_TO_TEX * projectorClipPosPrediv;
|
||||
vec2 projectorUv = projectorUvPrediv.xy / projectorUvPrediv.w;
|
||||
float projectorDepth = texture2D(projectorDepthSampler, projectorUv).r;
|
||||
float projectorClipDepth = projectorDepth * 2 - 1;
|
||||
|
||||
if (projectorClipPos.z < projectorClipDepth + 0.005) {
|
||||
vec4 projectorColor = texture2D(projectorColorSampler, vec2(projectorUv.s, 1 - projectorUv.t));
|
||||
float dotAttenuation = clamp((d - DOT_EPSILON) / (1 - DOT_EPSILON), 0, 1);
|
||||
float distanceAttenuation = clamp(1 - (linearDepth - START_FADE_DISTANCE) / (MAX_DISTANCE - START_FADE_DISTANCE), 0, 1);
|
||||
result = projectorColor * dotAttenuation * distanceAttenuation;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void main() {
|
||||
float depth = texture2D(Sampler0, texCoord).r;
|
||||
|
||||
// Check for no hit, e.g. sky, for early exit.
|
||||
if (depth >= 1) {
|
||||
discard;
|
||||
}
|
||||
|
||||
float clipDepth = depth * 2 - 1;
|
||||
vec3 worldPos = getWorldPos(texCoord, clipDepth);
|
||||
vec3 worldNormal = getNormal(worldPos);
|
||||
|
||||
vec4 colorAcc = vec4(0, 0, 0, 0);
|
||||
vec4 color;
|
||||
int accCount = 0;
|
||||
if (Count > 0 && getProjectorColor(worldPos, worldNormal, ProjectorCamera0, Sampler1, Sampler2, color)) {
|
||||
colorAcc += color;
|
||||
accCount += 1;
|
||||
}
|
||||
if (Count > 1 && getProjectorColor(worldPos, worldNormal, ProjectorCamera1, Sampler3, Sampler4, color)) {
|
||||
colorAcc += color;
|
||||
accCount += 1;
|
||||
}
|
||||
if (Count > 2 && getProjectorColor(worldPos, worldNormal, ProjectorCamera2, Sampler5, Sampler6, color)) {
|
||||
colorAcc += color;
|
||||
accCount += 1;
|
||||
}
|
||||
|
||||
// Check if we had any projections at all.
|
||||
if (accCount == 0) {
|
||||
discard;
|
||||
}
|
||||
|
||||
fragColor = colorAcc / accCount;
|
||||
}
|
||||
28
src/main/resources/assets/oc2/shaders/core/projectors.json
Normal file
28
src/main/resources/assets/oc2/shaders/core/projectors.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"blend": {
|
||||
"func": "add",
|
||||
"srcrgb": "srcalpha",
|
||||
"dstrgb": "1-srcalpha"
|
||||
},
|
||||
"vertex": "oc2:projectors",
|
||||
"fragment": "oc2:projectors",
|
||||
"attributes": ["Position", "UV0"],
|
||||
"samplers": [
|
||||
{"name": "Sampler0"},
|
||||
{"name": "Sampler1"},
|
||||
{"name": "Sampler2"},
|
||||
{"name": "Sampler3"},
|
||||
{"name": "Sampler4"},
|
||||
{"name": "Sampler5"},
|
||||
{"name": "Sampler6"}
|
||||
],
|
||||
"uniforms": [
|
||||
{"name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
|
||||
{"name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
|
||||
{"name": "InverseMainCamera", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
|
||||
{"name": "ProjectorCamera0", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
|
||||
{"name": "ProjectorCamera1", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
|
||||
{"name": "ProjectorCamera2", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
|
||||
{"name": "Count", "type": "int", "count": 1, "values": [0]}
|
||||
]
|
||||
}
|
||||
15
src/main/resources/assets/oc2/shaders/core/projectors.vsh
Normal file
15
src/main/resources/assets/oc2/shaders/core/projectors.vsh
Normal file
@@ -0,0 +1,15 @@
|
||||
#version 150
|
||||
|
||||
in vec3 Position;
|
||||
in vec2 UV0;
|
||||
|
||||
uniform mat4 ModelViewMat;
|
||||
uniform mat4 ProjMat;
|
||||
|
||||
out vec2 texCoord;
|
||||
|
||||
void main() {
|
||||
gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
|
||||
|
||||
texCoord = UV0;
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
{
|
||||
"minVersion": "0.8",
|
||||
"compatibilityLevel": "JAVA_17",
|
||||
"required": true,
|
||||
"package": "li.cil.oc2.common.mixin",
|
||||
"refmap": "oc2.refmap.json",
|
||||
"mixins": [
|
||||
"ChunkAccessMixin",
|
||||
"ChunkMapMixin"
|
||||
],
|
||||
"client": [
|
||||
"LevelRendererMixin",
|
||||
"MinecraftMixin",
|
||||
"RenderSystemMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
"minVersion": "0.8",
|
||||
"compatibilityLevel": "JAVA_17",
|
||||
"required": true,
|
||||
"package": "li.cil.oc2.common.mixin",
|
||||
"refmap": "oc2.refmap.json",
|
||||
"mixins": [
|
||||
"ChunkAccessMixin",
|
||||
"ChunkMapMixin"
|
||||
],
|
||||
"client": [
|
||||
"LevelRendererMixin",
|
||||
"MinecraftMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user