Skip to content

Commit 1e3a618

Browse files
CursedFlamesNotStirred
authored andcommitted
reimplement 3d loading screen for cubic worlds
1 parent 5c34aa8 commit 1e3a618

File tree

14 files changed

+627
-1
lines changed

14 files changed

+627
-1
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.github.opencubicchunks.cubicchunks.client.gui.render;
2+
3+
import io.github.opencubicchunks.cubicchunks.client.gui.render.pip.WorldLoadingCubeStatusesRenderer;
4+
import net.neoforged.bus.api.SubscribeEvent;
5+
import net.neoforged.fml.common.EventBusSubscriber;
6+
import net.neoforged.neoforge.client.event.RegisterPictureInPictureRenderersEvent;
7+
8+
@EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD)
9+
public class CCNeoForgeRenderingHooks {
10+
private CCNeoForgeRenderingHooks() {}
11+
12+
@SubscribeEvent
13+
public static void registerPipRenderers(RegisterPictureInPictureRenderersEvent event) {
14+
event.register(WorldLoadingCubeStatusesRenderer::new);
15+
}
16+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@ParametersAreNonnullByDefault
2+
@MethodsReturnNonnullByDefault
3+
package io.github.opencubicchunks.cubicchunks.client.gui.render;
4+
5+
import javax.annotation.ParametersAreNonnullByDefault;
6+
7+
import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault;
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package io.github.opencubicchunks.cubicchunks.client.gui.render.pip;
2+
3+
import java.util.EnumSet;
4+
5+
import com.mojang.blaze3d.ProjectionType;
6+
import com.mojang.blaze3d.systems.RenderSystem;
7+
import com.mojang.blaze3d.vertex.PoseStack;
8+
import com.mojang.blaze3d.vertex.VertexConsumer;
9+
import com.mojang.math.Axis;
10+
import io.github.opencubicchunks.cc_core.api.CubicConstants;
11+
import io.github.opencubicchunks.cc_core.utils.Coords;
12+
import io.github.opencubicchunks.cubicchunks.client.gui.render.state.pip.WorldLoadingCubeStatusesRenderState;
13+
import io.github.opencubicchunks.cubicchunks.client.gui.screens.CubicLevelLoadingScreen;
14+
import io.github.opencubicchunks.cubicchunks.server.level.progress.StoringCloProgressListener;
15+
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
16+
import net.minecraft.client.renderer.CachedPerspectiveProjectionMatrixBuffer;
17+
import net.minecraft.client.renderer.MultiBufferSource;
18+
import net.minecraft.client.renderer.RenderPipelines;
19+
import net.minecraft.client.renderer.RenderType;
20+
import net.minecraft.core.Direction;
21+
import net.minecraft.server.level.progress.StoringChunkProgressListener;
22+
import net.minecraft.util.ARGB;
23+
import net.minecraft.world.level.chunk.status.ChunkStatus;
24+
import org.joml.Vector3f;
25+
26+
/**
27+
* Picture-in-Picture renderer used to render the 3d loading animation used when loading into a cubic world.
28+
*/
29+
public class WorldLoadingCubeStatusesRenderer extends PictureInPictureRenderer<WorldLoadingCubeStatusesRenderState> {
30+
// "I want to essentially get rid of RenderType as a concept" - Dinnerbone
31+
// so this will probably be obsolete soon
32+
public static final RenderType WORLD_LOADING_CUBE_STATUSES = RenderType.create("cubicchunks_loading_cube_statuses",
33+
RenderType.TRANSIENT_BUFFER_SIZE, false, true, RenderPipelines.GUI, RenderType.CompositeState.builder().createCompositeState(false));
34+
35+
// PictureInPictureRenderer defaults to an orthographic projection matrix, so we make our own perspective projection matrix
36+
private final CachedPerspectiveProjectionMatrixBuffer perspectiveProjectionMatrixBuffer = new CachedPerspectiveProjectionMatrixBuffer(
37+
"world loading cubes", 0.05F, 100F);
38+
39+
private static final float FOV_DEGREES = 60F;
40+
41+
/** Distance between the center of the loading animation and the camera */
42+
private static final float CAMERA_DISTANCE = 20F;
43+
44+
/** Rotation of the loading animation about the X axis (rotating "downwards" towards the camera) */
45+
private static final float X_ROTATION_DEGREES = 30F;
46+
47+
private static final double Y_ROTATION_DEGREES_PER_SECOND = 40;
48+
49+
/** Constant controlling the size at which cubes are rendered */
50+
private static final float CUBE_BASE_SCALE = 0.12F;
51+
52+
private static final float CHUNK_Y_OFFSET = -30F;
53+
54+
private static final int CHUNK_ALPHA = 0x80;
55+
56+
private static final int DARKEN_PERCENTAGE_CUBE_FRONT_BACK = 20;
57+
private static final int DARKEN_PERCENTAGE_CUBE_SIDES = 30;
58+
private static final int DARKEN_PERCENTAGE_CUBE_BOTTOM = 40;
59+
60+
public WorldLoadingCubeStatusesRenderer(MultiBufferSource.BufferSource bufferSource) {
61+
super(bufferSource);
62+
}
63+
64+
@Override public void close() {
65+
super.close();
66+
67+
perspectiveProjectionMatrixBuffer.close();
68+
}
69+
70+
@Override public Class<WorldLoadingCubeStatusesRenderState> getRenderStateClass() {
71+
return WorldLoadingCubeStatusesRenderState.class;
72+
}
73+
74+
@SuppressWarnings("checkstyle:MagicNumber") // millis to seconds; degrees modulo 360
75+
private static float yRotationAngleDegrees(long milliseconds) {
76+
return (float) ((milliseconds * (Y_ROTATION_DEGREES_PER_SECOND / 1000)) % 360);
77+
}
78+
79+
@Override protected void renderToTexture(WorldLoadingCubeStatusesRenderState state, PoseStack poseStackUnused) {
80+
int width = state.x1() - state.x0();
81+
int height = state.y1() - state.y0();
82+
RenderSystem.setProjectionMatrix(perspectiveProjectionMatrixBuffer.getBuffer(width, height, FOV_DEGREES), ProjectionType.PERSPECTIVE);
83+
84+
// The PoseStack passed to this method is set up for the orthographic projection matrix;
85+
// as we're using perspective projection instead, we set up our own PoseStack.
86+
var poseStack = new PoseStack();
87+
88+
poseStack.translate(0f, 0f, -CAMERA_DISTANCE);
89+
poseStack.rotateAround(Axis.XP.rotationDegrees(X_ROTATION_DEGREES), 0.0F, 0.0F, 0.0F);
90+
poseStack.rotateAround(Axis.YP.rotationDegrees(yRotationAngleDegrees(System.currentTimeMillis())), 0.0F, 0.0F, 0.0F);
91+
var vertexConsumer = this.bufferSource.getBuffer(WORLD_LOADING_CUBE_STATUSES);
92+
renderCubesAndChunks(vertexConsumer, poseStack, state.chunkProgressListener(), state.scale());
93+
this.bufferSource.endBatch();
94+
}
95+
96+
@Override protected String getTextureLabel() {
97+
return "world loading cube statuses";
98+
}
99+
100+
private static void renderCubesAndChunks(
101+
VertexConsumer vertexConsumer, PoseStack poseStack, StoringChunkProgressListener progressListener, float scale
102+
) {
103+
int sectionRenderRadius = progressListener.getDiameter();
104+
drawChunks(vertexConsumer, poseStack, progressListener, scale, sectionRenderRadius);
105+
drawCubes(vertexConsumer, poseStack, progressListener, scale, Coords.sectionToCubeCeil(sectionRenderRadius));
106+
107+
}
108+
109+
private static void drawChunks(
110+
VertexConsumer vertexConsumer, PoseStack poseStack, StoringChunkProgressListener progressListener, float scale, int sectionRenderRadius
111+
) {
112+
for (int cdx = 0; cdx < sectionRenderRadius; cdx++) {
113+
for (int cdz = 0; cdz < sectionRenderRadius; cdz++) {
114+
ChunkStatus columnStatus = progressListener.getStatus(cdx, cdz);
115+
if (columnStatus == null) {
116+
continue;
117+
}
118+
int color = ARGB.color(CHUNK_ALPHA,
119+
CubicLevelLoadingScreen.STATUS_COLORS.getOrDefault(columnStatus, CubicLevelLoadingScreen.DEFAULT_STATUS_COLOR));
120+
// We render the chunks underneath the cubes by rendering a smaller cube with only the top face visible
121+
drawCube(vertexConsumer, poseStack, cdx - (float) sectionRenderRadius / 2, CHUNK_Y_OFFSET, cdz - (float) sectionRenderRadius / 2,
122+
CUBE_BASE_SCALE * scale / CubicConstants.DIAMETER_IN_SECTIONS, color, EnumSet.of(Direction.UP));
123+
}
124+
}
125+
}
126+
127+
private static void drawCubes(
128+
VertexConsumer vertexConsumer, PoseStack poseStack, StoringChunkProgressListener progressListener, float scale, int cubeRenderRadius
129+
) {
130+
EnumSet<Direction> renderFaces = EnumSet.noneOf(Direction.class);
131+
132+
var tracker = (StoringCloProgressListener) progressListener;
133+
for (int dx = -1; dx <= cubeRenderRadius + 1; dx++) {
134+
for (int dz = -1; dz <= cubeRenderRadius + 1; dz++) {
135+
for (int dy = -1; dy <= cubeRenderRadius + 1; dy++) {
136+
ChunkStatus status = tracker.cc_getStatus(dx, dy, dz);
137+
if (status == null) {
138+
continue;
139+
}
140+
renderFaces.clear();
141+
float alpha = CubicLevelLoadingScreen.STATUS_ALPHAS.getValue(status.getIndex());
142+
int color = ARGB.color(alpha,
143+
CubicLevelLoadingScreen.STATUS_COLORS.getOrDefault(status, CubicLevelLoadingScreen.DEFAULT_STATUS_COLOR));
144+
for (Direction value : Direction.values()) {
145+
ChunkStatus cubeStatus = tracker.cc_getStatus(dx + value.getStepX(), dy + value.getStepY(), dz + value.getStepZ());
146+
if (cubeStatus == null || !cubeStatus.isOrAfter(status)) {
147+
renderFaces.add(value);
148+
}
149+
}
150+
drawCube(vertexConsumer, poseStack, dx - (float) cubeRenderRadius / 2, dy - (float) cubeRenderRadius / 2,
151+
dz - (float) cubeRenderRadius / 2, CUBE_BASE_SCALE * scale, color, renderFaces);
152+
}
153+
}
154+
}
155+
}
156+
157+
private static void drawCube(
158+
VertexConsumer vertexConsumer, PoseStack poseStack, float x, float y, float z, float scale, int color, EnumSet<Direction> renderFaces
159+
) {
160+
float x0 = x * scale;
161+
float x1 = x0 + scale;
162+
float y0 = y * scale;
163+
float y1 = y0 + scale;
164+
float z0 = z * scale;
165+
float z1 = z0 + scale;
166+
if (renderFaces.contains(Direction.UP)) {
167+
// up face
168+
vertex(vertexConsumer, poseStack, x0, y1, z0, 0, 1, 0, color);
169+
vertex(vertexConsumer, poseStack, x0, y1, z1, 0, 1, 0, color);
170+
vertex(vertexConsumer, poseStack, x1, y1, z1, 0, 1, 0, color);
171+
vertex(vertexConsumer, poseStack, x1, y1, z0, 0, 1, 0, color);
172+
}
173+
if (renderFaces.contains(Direction.DOWN)) {
174+
int c = darken(color, DARKEN_PERCENTAGE_CUBE_BOTTOM);
175+
// down face
176+
vertex(vertexConsumer, poseStack, x1, y0, z0, 0, -1, 0, c);
177+
vertex(vertexConsumer, poseStack, x1, y0, z1, 0, -1, 0, c);
178+
vertex(vertexConsumer, poseStack, x0, y0, z1, 0, -1, 0, c);
179+
vertex(vertexConsumer, poseStack, x0, y0, z0, 0, -1, 0, c);
180+
}
181+
if (renderFaces.contains(Direction.EAST)) {
182+
int c = darken(color, DARKEN_PERCENTAGE_CUBE_SIDES);
183+
// right face
184+
vertex(vertexConsumer, poseStack, x1, y1, z0, 1, 0, 0, c);
185+
vertex(vertexConsumer, poseStack, x1, y1, z1, 1, 0, 0, c);
186+
vertex(vertexConsumer, poseStack, x1, y0, z1, 1, 0, 0, c);
187+
vertex(vertexConsumer, poseStack, x1, y0, z0, 1, 0, 0, c);
188+
}
189+
if (renderFaces.contains(Direction.WEST)) {
190+
int c = darken(color, DARKEN_PERCENTAGE_CUBE_SIDES);
191+
// left face
192+
vertex(vertexConsumer, poseStack, x0, y0, z0, -1, 0, 0, c);
193+
vertex(vertexConsumer, poseStack, x0, y0, z1, -1, 0, 0, c);
194+
vertex(vertexConsumer, poseStack, x0, y1, z1, -1, 0, 0, c);
195+
vertex(vertexConsumer, poseStack, x0, y1, z0, -1, 0, 0, c);
196+
}
197+
if (renderFaces.contains(Direction.NORTH)) {
198+
int c = darken(color, DARKEN_PERCENTAGE_CUBE_FRONT_BACK);
199+
// front face (facing camera)
200+
vertex(vertexConsumer, poseStack, x0, y1, z0, 0, 0, -1, c);
201+
vertex(vertexConsumer, poseStack, x1, y1, z0, 0, 0, -1, c);
202+
vertex(vertexConsumer, poseStack, x1, y0, z0, 0, 0, -1, c);
203+
vertex(vertexConsumer, poseStack, x0, y0, z0, 0, 0, -1, c);
204+
}
205+
if (renderFaces.contains(Direction.SOUTH)) {
206+
int c = darken(color, DARKEN_PERCENTAGE_CUBE_FRONT_BACK);
207+
// back face
208+
vertex(vertexConsumer, poseStack, x0, y0, z1, 0, 0, 1, c);
209+
vertex(vertexConsumer, poseStack, x1, y0, z1, 0, 0, 1, c);
210+
vertex(vertexConsumer, poseStack, x1, y1, z1, 0, 0, 1, c);
211+
vertex(vertexConsumer, poseStack, x0, y1, z1, 0, 0, 1, c);
212+
}
213+
}
214+
215+
@SuppressWarnings("checkstyle:MagicNumber") // 100 for percentage
216+
private static int darken(int color, int percentage) {
217+
int r = ARGB.red(color);
218+
r -= (r * percentage) / 100;
219+
int g = ARGB.green(color);
220+
g -= (g * percentage) / 100;
221+
int b = ARGB.blue(color);
222+
b -= (b * percentage) / 100;
223+
return ARGB.color(ARGB.alpha(color), r, g, b);
224+
}
225+
226+
// TODO do we have any use for the normal vector? it's currently unused
227+
private static void vertex(VertexConsumer vertexConsumer, PoseStack pose, float x, float y, float z, int nx, int ny, int nz, int color) {
228+
var vec = new Vector3f();
229+
pose.last().pose().transformPosition(x, y, z, vec);
230+
231+
vertexConsumer.addVertex(vec.x, vec.y, vec.z).setColor(color);
232+
}
233+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@ParametersAreNonnullByDefault
2+
@MethodsReturnNonnullByDefault
3+
package io.github.opencubicchunks.cubicchunks.client.gui.render.pip;
4+
5+
import javax.annotation.ParametersAreNonnullByDefault;
6+
7+
import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.github.opencubicchunks.cubicchunks.client.gui.render.state.pip;
2+
3+
import javax.annotation.Nullable;
4+
5+
import io.github.opencubicchunks.cubicchunks.client.gui.render.pip.WorldLoadingCubeStatusesRenderer;
6+
import net.minecraft.client.gui.navigation.ScreenRectangle;
7+
import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;
8+
import net.minecraft.server.level.progress.StoringChunkProgressListener;
9+
10+
/**
11+
* PiP render state for {@link WorldLoadingCubeStatusesRenderer}.
12+
* Stores a reference to the {@link StoringChunkProgressListener} used for tracking world load progress.
13+
*/
14+
public record WorldLoadingCubeStatusesRenderState(
15+
StoringChunkProgressListener chunkProgressListener, int x0, int y0, int x1, int y1, float scale, @Nullable ScreenRectangle scissorArea,
16+
@Nullable ScreenRectangle bounds
17+
) implements PictureInPictureRenderState {
18+
public WorldLoadingCubeStatusesRenderState(
19+
StoringChunkProgressListener chunkProgressListener, int x0, int y0, int x1, int y1, float scale, @Nullable ScreenRectangle scissorArea
20+
) {
21+
this(chunkProgressListener, x0, y0, x1, y1, scale, scissorArea, PictureInPictureRenderState.getBounds(x0, y0, x1, y1, scissorArea));
22+
}
23+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@ParametersAreNonnullByDefault
2+
@MethodsReturnNonnullByDefault
3+
package io.github.opencubicchunks.cubicchunks.client.gui.render.state.pip;
4+
5+
import javax.annotation.ParametersAreNonnullByDefault;
6+
7+
import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault;

0 commit comments

Comments
 (0)