Skip to content

Commit b784dae

Browse files
committed
Implement shader-based thruster plume rendering
1 parent 519f7b6 commit b784dae

25 files changed

Lines changed: 1441 additions & 158 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dev.propulsionteam.propulsionsimulated.client.render.plume;
2+
3+
import net.minecraft.core.Direction;
4+
5+
public record PlumeRenderParams(
6+
Direction direction,
7+
float power,
8+
float length,
9+
float radius,
10+
float alpha,
11+
float red,
12+
float green,
13+
float blue,
14+
PlumeShape shape
15+
) {
16+
public static PlumeRenderParams fire(Direction direction, float power, float length, float radius) {
17+
return new PlumeRenderParams(
18+
direction,
19+
power,
20+
length,
21+
radius,
22+
1.0f,
23+
1.0f,
24+
0.38f,
25+
0.045f,
26+
PlumeShape.SQUARE
27+
);
28+
}
29+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package dev.propulsionteam.propulsionsimulated.client.render.plume;
2+
3+
import com.mojang.blaze3d.platform.GlStateManager;
4+
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
5+
import com.mojang.blaze3d.vertex.VertexFormat;
6+
import net.minecraft.client.renderer.RenderStateShard;
7+
import net.minecraft.client.renderer.RenderType;
8+
9+
public final class PlumeRenderType extends RenderType {
10+
private static final RenderType PLUME = RenderType.create(
11+
"createpropulsion_plume",
12+
DefaultVertexFormat.POSITION_TEX_COLOR,
13+
VertexFormat.Mode.QUADS,
14+
4096,
15+
false,
16+
true,
17+
CompositeState.builder()
18+
.setShaderState(new ShaderStateShard(PlumeShaders::plume))
19+
.setTransparencyState(new TransparencyStateShard(
20+
"createpropulsion_plume_blend",
21+
() -> {
22+
com.mojang.blaze3d.systems.RenderSystem.enableBlend();
23+
com.mojang.blaze3d.systems.RenderSystem.blendFuncSeparate(
24+
GlStateManager.SourceFactor.SRC_ALPHA,
25+
GlStateManager.DestFactor.ONE,
26+
GlStateManager.SourceFactor.ONE,
27+
GlStateManager.DestFactor.ONE
28+
);
29+
},
30+
() -> {
31+
com.mojang.blaze3d.systems.RenderSystem.disableBlend();
32+
com.mojang.blaze3d.systems.RenderSystem.defaultBlendFunc();
33+
}
34+
))
35+
.setDepthTestState(RenderStateShard.LEQUAL_DEPTH_TEST)
36+
.setWriteMaskState(RenderStateShard.COLOR_WRITE)
37+
.setCullState(RenderStateShard.NO_CULL)
38+
.setLightmapState(RenderStateShard.NO_LIGHTMAP)
39+
.setOverlayState(RenderStateShard.NO_OVERLAY)
40+
.createCompositeState(false)
41+
);
42+
43+
private PlumeRenderType(String name, VertexFormat format, VertexFormat.Mode mode, int bufferSize, boolean affectsCrumbling, boolean sortOnUpload, Runnable setupState, Runnable clearState) {
44+
super(name, format, mode, bufferSize, affectsCrumbling, sortOnUpload, setupState, clearState);
45+
}
46+
47+
public static RenderType plume() {
48+
return PLUME;
49+
}
50+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package dev.propulsionteam.propulsionsimulated.client.render.plume;
2+
3+
import com.mojang.blaze3d.vertex.PoseStack;
4+
import com.mojang.blaze3d.vertex.VertexConsumer;
5+
import com.mojang.math.Axis;
6+
import net.minecraft.client.renderer.MultiBufferSource;
7+
import net.minecraft.client.renderer.ShaderInstance;
8+
import net.minecraft.core.Direction;
9+
import net.minecraft.util.Mth;
10+
import net.minecraft.world.phys.Vec3;
11+
import org.joml.Matrix4f;
12+
import org.joml.Quaternionf;
13+
14+
public final class PlumeRenderer {
15+
private PlumeRenderer() {
16+
}
17+
18+
public static void render(PoseStack ms, MultiBufferSource buffer, PlumeRenderParams params, float time) {
19+
Vec3 direction = new Vec3(
20+
params.direction().getStepX(),
21+
params.direction().getStepY(),
22+
params.direction().getStepZ()
23+
);
24+
render(ms, buffer, params, time, direction);
25+
}
26+
27+
public static void render(PoseStack ms, MultiBufferSource buffer, PlumeRenderParams params, float time, Vec3 direction) {
28+
if (params.power() <= 0.004f) return;
29+
if (params.alpha() <= 0.004f) return;
30+
if (params.length() <= 0.004f) return;
31+
if (params.radius() <= 0.004f) return;
32+
if (direction.lengthSqr() <= 1.0e-8d) return;
33+
34+
ShaderInstance shader = PlumeShaders.plume();
35+
if (shader != null) {
36+
set(shader, "Time", time);
37+
set(shader, "Power", params.power());
38+
}
39+
40+
Vec3 dir = direction.normalize();
41+
42+
ms.pushPose();
43+
rotateLocalDownTo(ms, dir);
44+
45+
VertexConsumer vc = buffer.getBuffer(PlumeRenderType.plume());
46+
47+
drawPlumeMesh(ms.last().pose(), vc, params, params.length(), params.radius(), 32, 18, params.alpha(), time, 0.0f);
48+
drawPlumeMesh(ms.last().pose(), vc, params, params.length() * 0.86f, params.radius() * 0.54f, 28, 14, params.alpha() * 0.82f, time, 0.35f);
49+
drawPlumeMesh(ms.last().pose(), vc, params, params.length() * 0.62f, params.radius() * 0.22f, 24, 10, params.alpha() * 0.74f, time, 0.7f);
50+
51+
ms.popPose();
52+
}
53+
54+
private static void rotateLocalDownTo(PoseStack ms, Direction direction) {
55+
switch (direction) {
56+
case DOWN -> {
57+
}
58+
case UP -> ms.mulPose(Axis.XP.rotationDegrees(180.0f));
59+
case NORTH -> ms.mulPose(Axis.XP.rotationDegrees(90.0f));
60+
case SOUTH -> ms.mulPose(Axis.XP.rotationDegrees(-90.0f));
61+
case WEST -> ms.mulPose(Axis.ZP.rotationDegrees(-90.0f));
62+
case EAST -> ms.mulPose(Axis.ZP.rotationDegrees(90.0f));
63+
}
64+
}
65+
66+
private static void rotateLocalDownTo(PoseStack ms, Vec3 direction) {
67+
Quaternionf q = new Quaternionf().rotationTo(
68+
0.0f,
69+
-1.0f,
70+
0.0f,
71+
(float) direction.x,
72+
(float) direction.y,
73+
(float) direction.z
74+
);
75+
ms.mulPose(q);
76+
}
77+
78+
private static void drawPlumeMesh(Matrix4f matrix, VertexConsumer vc, PlumeRenderParams params, float length, float radius, int segments, int rings, float alpha, float time, float phase) {
79+
for (int j = 0; j < rings; j++) {
80+
float t0 = (float) j / rings;
81+
float t1 = (float) (j + 1) / rings;
82+
83+
float y0 = -length * t0;
84+
float y1 = -length * t1;
85+
86+
float r0 = radiusAt(params.shape(), radius, t0, time, phase);
87+
float r1 = radiusAt(params.shape(), radius, t1, time, phase);
88+
89+
int a0 = alphaAt(alpha, t0);
90+
int a1 = alphaAt(alpha, t1);
91+
92+
for (int i = 0; i < segments; i++) {
93+
float u0 = (float) i / segments;
94+
float u1 = (float) (i + 1) / segments;
95+
96+
float[] p00 = point(params.shape(), r0, u0);
97+
float[] p01 = point(params.shape(), r0, u1);
98+
float[] p10 = point(params.shape(), r1, u0);
99+
float[] p11 = point(params.shape(), r1, u1);
100+
101+
v(vc, matrix, p00[0], y0, p00[1], u0, t0, a0, params);
102+
v(vc, matrix, p10[0], y1, p10[1], u0, t1, a1, params);
103+
v(vc, matrix, p11[0], y1, p11[1], u1, t1, a1, params);
104+
v(vc, matrix, p01[0], y0, p01[1], u1, t0, a0, params);
105+
}
106+
}
107+
}
108+
109+
private static float[] point(PlumeShape shape, float radius, float u) {
110+
if (shape == PlumeShape.SQUARE) return squarePoint(radius, u);
111+
return roundPoint(radius, u);
112+
}
113+
114+
private static float[] roundPoint(float radius, float u) {
115+
float angle = Mth.TWO_PI * u;
116+
return new float[] {
117+
Mth.cos(angle) * radius,
118+
Mth.sin(angle) * radius
119+
};
120+
}
121+
122+
private static float[] squarePoint(float radius, float u) {
123+
float p = (u - Mth.floor(u)) * 4.0f;
124+
125+
if (p < 1.0f) {
126+
return new float[] {
127+
Mth.lerp(p, radius, -radius),
128+
-radius
129+
};
130+
}
131+
132+
if (p < 2.0f) {
133+
return new float[] {
134+
-radius,
135+
Mth.lerp(p - 1.0f, -radius, radius)
136+
};
137+
}
138+
139+
if (p < 3.0f) {
140+
return new float[] {
141+
Mth.lerp(p - 2.0f, -radius, radius),
142+
radius
143+
};
144+
}
145+
146+
return new float[] {
147+
radius,
148+
Mth.lerp(p - 3.0f, radius, -radius)
149+
};
150+
}
151+
152+
private static float radiusAt(PlumeShape shape, float baseRadius, float t, float time, float phase) {
153+
if (shape == PlumeShape.ROUND) {
154+
float ionNeedle = 1.0f - smoothstep(0.18f, 1.0f, t) * 0.48f;
155+
float ionPulse = 1.0f + 0.008f * Mth.sin(time * 48.0f + phase * 13.0f + t * 24.0f);
156+
return baseRadius * ionNeedle * ionPulse;
157+
}
158+
159+
float nearNozzle = Mth.lerp(smoothstep(0.0f, 0.12f, t), 0.58f, 1.0f);
160+
float expansion = 1.0f + 0.42f * smoothstep(0.10f, 0.55f, t);
161+
float compression = 1.0f - 0.18f * smoothstep(0.55f, 0.82f, t);
162+
float tip = 1.0f - smoothstep(0.78f, 1.0f, t) * 0.82f;
163+
float pulse = 1.0f + 0.012f * Mth.sin(time * 34.0f + phase * 11.0f + t * 18.0f);
164+
return baseRadius * nearNozzle * expansion * compression * tip * pulse;
165+
}
166+
167+
private static int alphaAt(float alpha, float t) {
168+
float fadeIn = smoothstep(0.0f, 0.045f, t);
169+
float fadeOut = 1.0f - smoothstep(0.80f, 1.0f, t);
170+
float body = 1.0f - 0.22f * smoothstep(0.45f, 0.88f, t);
171+
return Mth.clamp((int) (alpha * 255.0f * fadeIn * fadeOut * body), 0, 255);
172+
}
173+
174+
private static float smoothstep(float edge0, float edge1, float x) {
175+
float t = Mth.clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f);
176+
return t * t * (3.0f - 2.0f * t);
177+
}
178+
179+
private static void v(VertexConsumer vc, Matrix4f matrix, float x, float y, float z, float u, float v, int alpha, PlumeRenderParams params) {
180+
vc.addVertex(matrix, x, y, z)
181+
.setUv(u, v)
182+
.setColor(
183+
Mth.clamp((int) (params.red() * 255.0f), 0, 255),
184+
Mth.clamp((int) (params.green() * 255.0f), 0, 255),
185+
Mth.clamp((int) (params.blue() * 255.0f), 0, 255),
186+
alpha
187+
);
188+
}
189+
190+
private static void set(ShaderInstance shader, String name, float value) {
191+
var uniform = shader.getUniform(name);
192+
if (uniform != null) uniform.set(value);
193+
}
194+
195+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package dev.propulsionteam.propulsionsimulated.client.render.plume;
2+
3+
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
4+
import dev.propulsionteam.propulsionsimulated.CreatePropulsion;
5+
import net.minecraft.client.renderer.ShaderInstance;
6+
import net.minecraft.resources.ResourceLocation;
7+
import net.neoforged.api.distmarker.Dist;
8+
import net.neoforged.bus.api.SubscribeEvent;
9+
import net.neoforged.fml.common.EventBusSubscriber;
10+
import net.neoforged.neoforge.client.event.RegisterShadersEvent;
11+
12+
import java.io.IOException;
13+
14+
@EventBusSubscriber(modid = CreatePropulsion.ID, bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
15+
public final class PlumeShaders {
16+
private static ShaderInstance plume;
17+
18+
private PlumeShaders() {
19+
}
20+
21+
@SubscribeEvent
22+
public static void registerShaders(RegisterShadersEvent event) throws IOException {
23+
event.registerShader(
24+
new ShaderInstance(
25+
event.getResourceProvider(),
26+
ResourceLocation.fromNamespaceAndPath(CreatePropulsion.ID, "plume"),
27+
DefaultVertexFormat.POSITION_TEX_COLOR
28+
),
29+
shader -> plume = shader
30+
);
31+
}
32+
33+
public static ShaderInstance plume() {
34+
return plume;
35+
}
36+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package dev.propulsionteam.propulsionsimulated.client.render.plume;
2+
3+
public enum PlumeShape {
4+
ROUND,
5+
SQUARE
6+
}

0 commit comments

Comments
 (0)