Files
CustomNPC-Plus/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java
T
2026-02-12 13:55:56 -05:00

306 lines
11 KiB
Java

package kamkeel.npcs.client.renderer.lightning;
import net.minecraft.client.renderer.Tessellator;
import org.lwjgl.opengl.GL11;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
/**
* Renders lightning arcs attached to an entity (in local space).
* Supports both instant arcs and fading persistent bolts that move with the entity.
*/
public class AttachedLightningRenderer {
private static final Random rand = new Random();
/**
* Data for a single lightning bolt with fading support.
*/
public static class LightningArc {
public List<double[]> points;
public int age;
public int maxAge;
public int outerColor;
public int innerColor;
public LightningArc(List<double[]> points, int maxAge, int outerColor, int innerColor) {
this.points = points;
this.age = 0;
this.maxAge = maxAge;
this.outerColor = outerColor;
this.innerColor = innerColor;
}
public float getAlphaMultiplier() {
// Fade out over lifetime
return 1.0f - ((float) age / (float) maxAge);
}
public boolean isDead() {
return age >= maxAge;
}
public void tick() {
age++;
}
}
/**
* Storage for persistent lightning arcs per entity.
* Call updateAndRender() each frame from the entity's renderer.
*/
public static class LightningState {
private final List<LightningArc> arcs = new ArrayList<>();
private float spawnAccumulator = 0;
/**
* Update arc ages, remove dead arcs, spawn new ones based on density.
*
* @param density Arcs per tick (can be fractional)
* @param radius Max arc distance from center
* @param outerColor Outer glow color
* @param innerColor Inner core color
* @param maxAge How many ticks arcs live (for fading)
*/
public void update(float density, float radius, int outerColor, int innerColor, int maxAge) {
// Age existing arcs and remove dead ones
Iterator<LightningArc> iter = arcs.iterator();
while (iter.hasNext()) {
LightningArc arc = iter.next();
arc.tick();
if (arc.isDead()) {
iter.remove();
}
}
// Spawn new arcs based on density
spawnAccumulator += density;
while (spawnAccumulator >= 1.0f) {
spawnAccumulator -= 1.0f;
arcs.add(createArc(radius, outerColor, innerColor, maxAge));
}
}
/**
* Age existing arcs and remove dead ones without spawning new arcs.
* Use with addArc() for custom arc generation.
*/
public void tick() {
Iterator<LightningArc> iter = arcs.iterator();
while (iter.hasNext()) {
LightningArc arc = iter.next();
arc.tick();
if (arc.isDead()) {
iter.remove();
}
}
}
/**
* Add a custom arc to the state.
*/
public void addArc(LightningArc arc) {
arcs.add(arc);
}
/**
* Render all active arcs in local space.
* Call this after glTranslated to entity position.
* Note: Expects the caller to have already set up GL state (disabled textures,
* disabled lighting, disabled culling, enabled blending). This avoids nesting
* glPushAttrib calls which can overflow the GL attrib stack (depth 16) when
* other mods also push attribs during entity rendering.
*/
public void render() {
if (arcs.isEmpty()) return;
GL11.glDepthMask(false);
Tessellator tess = Tessellator.instance;
for (LightningArc arc : arcs) {
float alphaMult = arc.getAlphaMultiplier();
// Render outer glow
renderBoltPath(tess, arc.points, arc.outerColor, 0.025f, 0.35f * alphaMult);
// Render inner core
renderBoltPath(tess, arc.points, arc.innerColor, 0.012f, 0.7f * alphaMult);
}
GL11.glDepthMask(true);
}
public int getArcCount() {
return arcs.size();
}
}
/**
* Create a new lightning arc from center to a random point.
*/
private static LightningArc createArc(float radius, int outerColor, int innerColor, int maxAge) {
// Random end point on sphere
double theta = rand.nextDouble() * Math.PI * 2;
double phi = Math.acos(2 * rand.nextDouble() - 1);
double r = radius * (0.5 + rand.nextDouble() * 0.5);
double endX = r * Math.sin(phi) * Math.cos(theta);
double endY = r * Math.sin(phi) * Math.sin(theta);
double endZ = r * Math.cos(phi);
int segments = 4 + rand.nextInt(3);
float displacement = radius * 0.25f;
List<double[]> points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement);
return new LightningArc(points, maxAge, outerColor, innerColor);
}
/**
* Generate a jagged lightning path between two points.
*/
private static List<double[]> generateLightningPath(double x1, double y1, double z1,
double x2, double y2, double z2,
int segments, float displacement) {
List<double[]> points = new ArrayList<>();
points.add(new double[]{x1, y1, z1});
double dx = x2 - x1;
double dy = y2 - y1;
double dz = z2 - z1;
// Create perpendicular vectors for displacement
Vec3d dir = new Vec3d(dx, dy, dz);
Vec3d perp1 = dir.copy().perpendicular().normalize();
Vec3d perp2 = dir.copy().crossProduct(perp1).normalize();
for (int i = 1; i < segments; i++) {
double t = (double) i / segments;
double px = x1 + dx * t;
double py = y1 + dy * t;
double pz = z1 + dz * t;
// Displacement strongest in middle
float strength = (float) (1.0 - Math.abs(t - 0.5) * 2.0) * displacement;
double offset1 = (rand.nextDouble() - 0.5) * 2 * strength;
double offset2 = (rand.nextDouble() - 0.5) * 2 * strength;
px += perp1.x * offset1 + perp2.x * offset2;
py += perp1.y * offset1 + perp2.y * offset2;
pz += perp1.z * offset1 + perp2.z * offset2;
points.add(new double[]{px, py, pz});
}
points.add(new double[]{x2, y2, z2});
return points;
}
/**
* Render the lightning path as quads.
*/
private static void renderBoltPath(Tessellator tess, List<double[]> points, int color, float width, float alpha) {
if (alpha <= 0.01f) return;
Color c = new Color(color);
int r = c.getRed();
int g = c.getGreen();
int b = c.getBlue();
int a = Math.min(255, Math.max(0, (int) (alpha * 255)));
for (int i = 0; i < points.size() - 1; i++) {
double[] p1 = points.get(i);
double[] p2 = points.get(i + 1);
double segDx = p2[0] - p1[0];
double segDy = p2[1] - p1[1];
double segDz = p2[2] - p1[2];
Vec3d segDir = new Vec3d(segDx, segDy, segDz);
Vec3d perp = segDir.copy().perpendicular().normalize().multiply(width);
// First quad
tess.startDrawingQuads();
tess.setBrightness(0xF000F0);
tess.setColorRGBA(r, g, b, a);
tess.addVertex(p1[0] - perp.x, p1[1] - perp.y, p1[2] - perp.z);
tess.addVertex(p1[0] + perp.x, p1[1] + perp.y, p1[2] + perp.z);
tess.addVertex(p2[0] + perp.x, p2[1] + perp.y, p2[2] + perp.z);
tess.addVertex(p2[0] - perp.x, p2[1] - perp.y, p2[2] - perp.z);
tess.draw();
// Second quad (perpendicular for 3D look)
Vec3d perp2 = segDir.copy().crossProduct(perp).normalize().multiply(width);
tess.startDrawingQuads();
tess.setBrightness(0xF000F0);
tess.setColorRGBA(r, g, b, a);
tess.addVertex(p1[0] - perp2.x, p1[1] - perp2.y, p1[2] - perp2.z);
tess.addVertex(p1[0] + perp2.x, p1[1] + perp2.y, p1[2] + perp2.z);
tess.addVertex(p2[0] + perp2.x, p2[1] + perp2.y, p2[2] + perp2.z);
tess.addVertex(p2[0] - perp2.x, p2[1] - perp2.y, p2[2] - perp2.z);
tess.draw();
}
}
/**
* Create a lightning arc between two arbitrary points.
*/
public static LightningArc createArcBetween(double x1, double y1, double z1,
double x2, double y2, double z2,
float displacement, int outerColor, int innerColor, int maxAge) {
int segments = 4 + rand.nextInt(3);
List<double[]> points = generateLightningPath(x1, y1, z1, x2, y2, z2, segments, displacement);
return new LightningArc(points, maxAge, outerColor, innerColor);
}
// ==================== INSTANT RENDERING (no persistence) ====================
/**
* Render a single instant lightning arc (no fading, drawn this frame only).
* Expects the caller to have already set up GL state (disabled textures,
* disabled lighting, disabled culling, enabled blending).
*/
public static void renderInstantArc(float radius, int outerColor, int innerColor) {
double theta = rand.nextDouble() * Math.PI * 2;
double phi = Math.acos(2 * rand.nextDouble() - 1);
double r = radius * (0.5 + rand.nextDouble() * 0.5);
double endX = r * Math.sin(phi) * Math.cos(theta);
double endY = r * Math.sin(phi) * Math.sin(theta);
double endZ = r * Math.cos(phi);
int segments = 4 + rand.nextInt(3);
float displacement = radius * 0.25f;
List<double[]> points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement);
GL11.glDepthMask(false);
Tessellator tess = Tessellator.instance;
renderBoltPath(tess, points, outerColor, 0.025f, 0.35f);
renderBoltPath(tess, points, innerColor, 0.012f, 0.7f);
GL11.glDepthMask(true);
}
/**
* Render multiple instant arcs.
*/
public static void renderCracklingLightning(int count, float radius, int outerColor, int innerColor) {
for (int i = 0; i < count; i++) {
renderInstantArc(radius, outerColor, innerColor);
}
}
}