feat(mod): align ElytraTrails and Blizzard with evo (real wing-tip matrices, ribbon styles, ground-aware snow, whiteout fog, hide vanilla weather), spec tests
This commit is contained in:
parent
9661ff638d
commit
60b4fedb29
34 changed files with 1937 additions and 623 deletions
|
|
@ -0,0 +1,66 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.effects.elytratrails;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.ribbon.RibbonPoint;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.ribbon.RibbonShape;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.ribbon.TrailRibbon;
|
||||
|
||||
/** Вид ленты шлейфа элитры (по evo): горизонтальная лента с сужением, скоростью, плавным стартом и колебаниями. */
|
||||
final class ElytraRibbonStyle implements TrailRibbon.Style {
|
||||
|
||||
static final float GLOW_WIDTH_SCALE = 2.4f;
|
||||
|
||||
private final RibbonShape.Taper taper;
|
||||
private final float taperAmount;
|
||||
private final RibbonShape.Motion motion;
|
||||
private final float waveSize;
|
||||
private final float waveSpeed;
|
||||
private final boolean speedReactive;
|
||||
private final boolean fadeStart;
|
||||
private final boolean softEdges;
|
||||
private final boolean glow;
|
||||
|
||||
ElytraRibbonStyle(RibbonShape.Taper taper, float taperAmount, RibbonShape.Motion motion, float waveSize,
|
||||
float waveSpeed, boolean speedReactive, boolean fadeStart, boolean softEdges, boolean glow) {
|
||||
this.taper = taper;
|
||||
this.taperAmount = taperAmount;
|
||||
this.motion = motion;
|
||||
this.waveSize = waveSize;
|
||||
this.waveSpeed = waveSpeed;
|
||||
this.speedReactive = speedReactive;
|
||||
this.fadeStart = fadeStart;
|
||||
this.softEdges = softEdges;
|
||||
this.glow = glow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean horizontal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean softEdges() {
|
||||
return softEdges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float glowScale() {
|
||||
return glow ? GLOW_WIDTH_SCALE : 0.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double halfWidth(float width, int i, int n, RibbonPoint p, float life) {
|
||||
return width * 0.5 * RibbonShape.taper(taper, taperAmount, RibbonShape.tailness(i, n))
|
||||
* RibbonShape.speedWidth(speedReactive, p.speed());
|
||||
}
|
||||
|
||||
@Override
|
||||
public float alpha(int i, int n, RibbonPoint p, float life, float cameraDistance) {
|
||||
return life * RibbonShape.headRamp(fadeStart, i, n) * RibbonShape.nearFade(cameraDistance)
|
||||
* RibbonShape.speedAlpha(speedReactive, p.speed());
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] motion(RibbonPoint p) {
|
||||
return motion == RibbonShape.Motion.STRAIGHT ? null : RibbonShape.motion(motion, p.ms(), waveSize, waveSpeed);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,12 +8,16 @@ import dev.loki.lovisual.features.module.core.Module;
|
|||
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.elytratrails.pose.ElytraPoses;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.elytratrails.pose.WingTipLocator;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.ribbon.RibbonShape;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.ribbon.TrackBuffer;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.ribbon.TrailRibbon;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
|
|
@ -30,12 +34,27 @@ public class ElytraTrails extends Module {
|
|||
|
||||
private final WingTrailStore store = new WingTrailStore();
|
||||
|
||||
private static final double MAX_TRACK_DISTANCE_SQ = 16384.0;
|
||||
|
||||
private final BooleanValue own = bool("elytraTrailsOwn", "own", true);
|
||||
private final BooleanValue others = bool("elytraTrailsOthers", "others", true);
|
||||
private final NumberValue<Integer> duration = num("elytraTrailsDuration", "duration", 700, 150, 3000);
|
||||
private final NumberValue<Float> width = num("elytraTrailsWidth", "width", 0.35f, 0.05f, 1.5f);
|
||||
private final NumberValue<Integer> duration = num("elytraTrailsDuration", "duration", 1200, 200, 4000);
|
||||
private final NumberValue<Float> width = num("elytraTrailsWidth", "width", 0.22f, 0.02f, 1.5f);
|
||||
private final ModeValue taper = modeSetting("elytraTrailsTaper", "taper", "Grow", "Uniform", "Grow", "Shrink");
|
||||
private final NumberValue<Float> taperAmount = visibleWhen(
|
||||
num("elytraTrailsTaperAmount", "taper_amount", 1.8f, 1.25f, 8.0f), () -> !"Uniform".equals(taper.get()));
|
||||
private final ModeValue motion = modeSetting("elytraTrailsMotion", "motion", "Straight", "Straight", "Wave", "Helix");
|
||||
private final NumberValue<Float> waveSize = visibleWhen(
|
||||
num("elytraTrailsWaveSize", "wave_size", 0.35f, 0.05f, 2.0f), () -> !"Straight".equals(motion.get()));
|
||||
private final NumberValue<Float> waveSpeed = visibleWhen(
|
||||
num("elytraTrailsWaveSpeed", "wave_speed", 3.0f, 0.5f, 12.0f), () -> !"Straight".equals(motion.get()));
|
||||
private final BooleanValue fadeStart = bool("elytraTrailsFadeStart", "fade_start", true);
|
||||
private final BooleanValue speedReactive = bool("elytraTrailsSpeedReactive", "speed_reactive", true);
|
||||
private final BooleanValue glow = bool("elytraTrailsGlow", "glow", true);
|
||||
private final BooleanValue softEdges = bool("elytraTrailsSoftEdges", "soft_edges", true);
|
||||
private final BooleanValue fill = bool("elytraTrailsFill", "fill", true);
|
||||
private final BooleanValue lines = bool("elytraTrailsLines", "lines", false);
|
||||
private final NumberValue<Float> lineWidth = visibleWhen(num("elytraTrailsLineWidth", "line_width", 2.0f, 0.5f, 5.0f), lines::get);
|
||||
private final NumberValue<Float> lineWidth = visibleWhen(num("elytraTrailsLineWidth", "line_width", 2.0f, 1.0f, 5.0f), lines::get);
|
||||
private final BooleanValue depthTest = bool("elytraTrailsDepth", "depth_test", true);
|
||||
private final ModeValue colorMode = modeSetting("elytraTrailsColorMode", "color_mode", "DoubleColor",
|
||||
"Static", "Rainbow", "LightRainbow", "Sky", "Fade", "DoubleColor", "Analogous", "Theme");
|
||||
|
|
@ -60,13 +79,16 @@ public class ElytraTrails extends Module {
|
|||
if (!isEnabled() || mc.level == null || mc.gameRenderer.mainCamera() == null) return;
|
||||
long now = GameClock.millis();
|
||||
long life = duration.get();
|
||||
Vec3 cam = mc.gameRenderer.mainCamera().position();
|
||||
for (Player player : mc.level.players()) {
|
||||
if (!others.get() && player != mc.player) continue;
|
||||
if (!player.isAlive() || !player.isFallFlying()) continue;
|
||||
WingTipLocator.Tips tips = WingTipLocator.tips(player.getEyePosition(tickDelta),
|
||||
player.getViewYRot(tickDelta), player.getViewXRot(tickDelta));
|
||||
store.sample(player.getUUID(), WingTrailStore.LEFT, tips.left().x, tips.left().y, tips.left().z, now, life);
|
||||
store.sample(player.getUUID(), WingTrailStore.RIGHT, tips.right().x, tips.right().y, tips.right().z, now, life);
|
||||
if (!(player instanceof AbstractClientPlayer wearer)) continue;
|
||||
if (!wearer.isFallFlying() || wearer.isInvisible() || !wearer.isAlive()) continue;
|
||||
if (!(wearer == mc.player ? own.get() : others.get())) continue;
|
||||
if (wearer.distanceToSqr(cam) > MAX_TRACK_DISTANCE_SQ) continue;
|
||||
WingTipLocator.Tips tips = WingTipLocator.tips(ElytraPoses.of(wearer, tickDelta));
|
||||
float speed = ElytraPoses.speed(wearer);
|
||||
store.sample(wearer.getUUID(), WingTrailStore.LEFT, tips.left().x, tips.left().y, tips.left().z, now, life, speed);
|
||||
store.sample(wearer.getUUID(), WingTrailStore.RIGHT, tips.right().x, tips.right().y, tips.right().z, now, life, speed);
|
||||
}
|
||||
store.prune(now, life);
|
||||
List<TrackBuffer> tracks = store.snapshot();
|
||||
|
|
@ -77,10 +99,13 @@ public class ElytraTrails extends Module {
|
|||
int secondary = color2.getArgb();
|
||||
int speed = colorSpeed.get();
|
||||
AnimatedRenderColors.Mode mode = AnimatedRenderColors.modeOf(colorMode.get());
|
||||
Vec3 cam = mc.gameRenderer.mainCamera().position();
|
||||
TrailRibbon.render(renderer, tracks, now, life, width.get(), true, glow.get(), lines.get(), lineWidth.get(),
|
||||
ElytraRibbonStyle style = new ElytraRibbonStyle(RibbonShape.Taper.of(taper.get()), taperAmount.get(),
|
||||
RibbonShape.Motion.of(motion.get()), waveSize.get(), waveSpeed.get(), speedReactive.get(),
|
||||
fadeStart.get(), softEdges.get(), glow.get());
|
||||
TrailRibbon.render(renderer, tracks, now, life, width.get(), fill.get(), false, lines.get(), lineWidth.get(),
|
||||
depthTest.get(), cam,
|
||||
(i, n) -> AnimatedRenderColors.resolve(mode, speed,
|
||||
phase + Math.round(i / (float) Math.max(1, n - 1) * 180), primary, secondary, true));
|
||||
phase + Math.round(i / (float) Math.max(1, n - 1) * 180), primary, secondary, true),
|
||||
style);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.effects.elytratrails;
|
||||
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* Концы крыльев планирующего игрока. Точка привязки лежит позади головы вдоль направления полёта,
|
||||
* размах крыльев перпендикулярен ему и всегда горизонтален (без крена). Чистая математика.
|
||||
*/
|
||||
public final class WingTipLocator {
|
||||
|
||||
/** Половина размаха: расстояние от оси тела до конца крыла, блоки. */
|
||||
static final float HALF_SPAN = 1.0f;
|
||||
/** Корни крыльев лежат позади глаз на этом расстоянии вдоль направления полёта. */
|
||||
static final float BACK = 0.3f;
|
||||
/** Смещение по «верху» тела относительно глаз (корни крыльев чуть ниже глаз). */
|
||||
static final float HEIGHT = -0.15f;
|
||||
|
||||
private WingTipLocator() {
|
||||
}
|
||||
|
||||
public record Tips(Vec3 left, Vec3 right) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param eye позиция глаз (интерполированная)
|
||||
* @param yawDeg рыскание в градусах (0 смотрит на +Z)
|
||||
* @param pitchDeg наклон в градусах, положительный вниз
|
||||
*/
|
||||
public static Tips tips(Vec3 eye, float yawDeg, float pitchDeg) {
|
||||
double yaw = Math.toRadians(yawDeg);
|
||||
double pitch = Math.toRadians(pitchDeg);
|
||||
Vec3 forward = new Vec3(-Math.sin(yaw) * Math.cos(pitch), -Math.sin(pitch), Math.cos(yaw) * Math.cos(pitch));
|
||||
// Правая рука игрока зависит только от рыскания, поэтому пара не вырождается при взгляде вверх и вниз.
|
||||
Vec3 right = new Vec3(-Math.cos(yaw), 0.0, -Math.sin(yaw));
|
||||
Vec3 up = right.cross(forward);
|
||||
Vec3 anchor = eye.subtract(forward.scale(BACK)).add(up.scale(HEIGHT));
|
||||
return new Tips(anchor.subtract(right.scale(HALF_SPAN)), anchor.add(right.scale(HALF_SPAN)));
|
||||
}
|
||||
}
|
||||
|
|
@ -20,10 +20,19 @@ final class WingTrailStore {
|
|||
tracks.clear();
|
||||
}
|
||||
|
||||
/** Минимальный шаг записи (блоки), скачок и пауза, после которых лента начинается заново (как у evo). */
|
||||
static final double MIN_STEP = 0.35;
|
||||
static final double MAX_STEP = 8.0;
|
||||
static final long GAP_MS = 300L;
|
||||
|
||||
void sample(UUID id, int side, double x, double y, double z, long now, long lifeMs) {
|
||||
sample(id, side, x, y, z, now, lifeMs, 0.0f);
|
||||
}
|
||||
|
||||
void sample(UUID id, int side, double x, double y, double z, long now, long lifeMs, float speed) {
|
||||
TrackBuffer[] pair = tracks.computeIfAbsent(id, ignored -> new TrackBuffer[2]);
|
||||
if (pair[side] == null) pair[side] = new TrackBuffer();
|
||||
pair[side].add(x, y, z, now, lifeMs);
|
||||
pair[side].addContinuous(x, y, z, now, lifeMs, speed, MIN_STEP, MAX_STEP, GAP_MS);
|
||||
}
|
||||
|
||||
/** Убирает устаревшие точки; опустевшие ленты и игроки без лент удаляются. */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.effects.elytratrails.pose;
|
||||
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Собирает {@link WingTipLocator.Pose} из живого игрока так же, как это делает отрисовка элитры. */
|
||||
public final class ElytraPoses {
|
||||
|
||||
private ElytraPoses() {
|
||||
}
|
||||
|
||||
public static WingTipLocator.Pose of(AbstractClientPlayer player, float partial) {
|
||||
Vec3 pos = player.getPosition(partial);
|
||||
Vec3 delta = player.avatarState().deltaMovementOnPreviousTick().lerp(player.getDeltaMovement(), partial);
|
||||
Vec3 view = player.getViewVector(partial);
|
||||
float flying = WingTipLocator.flyingYRot(delta.x, delta.z, view.x, view.z);
|
||||
float bodyYaw = Mth.rotLerp(partial, player.yBodyRotO, player.yBodyRot);
|
||||
var wing = player.elytraAnimationState;
|
||||
return new WingTipLocator.Pose(pos.x, pos.y, pos.z, bodyYaw, player.getXRot(partial), player.getScale(),
|
||||
player.isCrouching(), player.isAutoSpinAttack(), player.getFallFlyingTicks() + partial, flying,
|
||||
player.tickCount + partial, wing.getRotX(partial), wing.getRotY(partial), wing.getRotZ(partial));
|
||||
}
|
||||
|
||||
/** Скорость игрока за тик, блоки (для ширины и прозрачности ленты). */
|
||||
public static float speed(AbstractClientPlayer player) {
|
||||
return (float) player.getDeltaMovement().length();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.effects.elytratrails.pose;
|
||||
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/**
|
||||
* Концы крыльев элитры по настоящей модели: та же цепочка преобразований, что у отрисовки элитры на теле
|
||||
* игрока (масштаб, поворот корпуса, наклон в полёте, поворот по траектории, крыло с анимацией взмаха),
|
||||
* как в evo. Чистая математика без Minecraft-классов, кроме {@link Vec3}.
|
||||
*/
|
||||
public final class WingTipLocator {
|
||||
|
||||
static final float MODEL_SCALE = 0.9375f;
|
||||
static final float BODY_OFFSET_Y = -1.501f;
|
||||
static final float WINGS_OFFSET_Z = 0.125f;
|
||||
static final float WING_PIVOT_X = 0.3125f;
|
||||
static final float WING_CROUCH_Y = 0.1875f;
|
||||
static final float TIP_X = 0.6875f;
|
||||
static final float TIP_Y = 1.3125f;
|
||||
static final float TIP_Z = 0.1875f;
|
||||
static final float SPIN_ATTACK_DEGREES = -75.0f;
|
||||
static final float FALL_FLYING_PITCH_OFFSET = -90.0f;
|
||||
static final float BODY_ROT_OFFSET = 180.0f;
|
||||
static final float FALL_FLYING_DIVISOR = 100.0f;
|
||||
static final float FLIGHT_ROT_EPSILON = 1.0E-5f;
|
||||
|
||||
private static final float DEG = (float) (Math.PI / 180.0);
|
||||
|
||||
private WingTipLocator() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Состояние игрока для расчёта.
|
||||
*
|
||||
* @param x,y,z интерполированная позиция (ноги)
|
||||
* @param bodyYawDeg интерполированное рыскание корпуса, градусы
|
||||
* @param xRotDeg наклон взгляда, градусы
|
||||
* @param scale масштаб сущности (атрибут размера)
|
||||
* @param spinAttack крутится трезубцем (riptide)
|
||||
* @param fallFlyingTicks сколько тиков планирует (с долей кадра)
|
||||
* @param flyingYRot см. {@link #flyingYRot}
|
||||
* @param spinTime tickCount + partialTick для вращения riptide
|
||||
* @param rotX,rotY,rotZ анимация крыла: у правого крыла знаки rotY и rotZ меняются
|
||||
*/
|
||||
public record Pose(double x, double y, double z, float bodyYawDeg, float xRotDeg, float scale,
|
||||
boolean crouching, boolean spinAttack, float fallFlyingTicks, float flyingYRot,
|
||||
float spinTime, float rotX, float rotY, float rotZ) {
|
||||
}
|
||||
|
||||
public record Tips(Vec3 left, Vec3 right) {
|
||||
}
|
||||
|
||||
/** Доля «лёжа»: 0 в начале планирования, 1 после десяти тиков. */
|
||||
public static float fallFlyingScale(float fallFlyingTicks) {
|
||||
float f = fallFlyingTicks * fallFlyingTicks / FALL_FLYING_DIVISOR;
|
||||
return f < 0.0f ? 0.0f : Math.min(f, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Поворот корпуса вокруг вертикали при лёгком рысканьи: угол между горизонтальной скоростью и взглядом,
|
||||
* со знаком по векторному произведению; 0, если игрок почти стоит или смотрит вертикально.
|
||||
*/
|
||||
public static float flyingYRot(double deltaX, double deltaZ, double viewX, double viewZ) {
|
||||
double dl = deltaX * deltaX + deltaZ * deltaZ;
|
||||
double vl = viewX * viewX + viewZ * viewZ;
|
||||
if (dl <= 1.0E-5 || vl <= 1.0E-5) return 0.0f;
|
||||
double dot = (deltaX * viewX + deltaZ * viewZ) / Math.sqrt(dl * vl);
|
||||
double cross = deltaX * viewZ - deltaZ * viewX;
|
||||
return (float) (Math.signum(cross) * Math.acos(Math.min(1.0, Math.abs(dot))));
|
||||
}
|
||||
|
||||
public static Tips tips(Pose p) {
|
||||
Matrix4f body = new Matrix4f().scale(p.scale()).rotateY((BODY_ROT_OFFSET - p.bodyYawDeg()) * DEG);
|
||||
if (p.spinAttack()) {
|
||||
body.rotateX((FALL_FLYING_PITCH_OFFSET - p.xRotDeg()) * DEG).rotateY(p.spinTime() * SPIN_ATTACK_DEGREES * DEG);
|
||||
} else {
|
||||
body.rotateX(fallFlyingScale(p.fallFlyingTicks()) * (FALL_FLYING_PITCH_OFFSET - p.xRotDeg()) * DEG);
|
||||
if (Math.abs(p.flyingYRot()) > FLIGHT_ROT_EPSILON) body.rotateY(p.flyingYRot());
|
||||
}
|
||||
body.scale(-1.0f, -1.0f, 1.0f).scale(MODEL_SCALE).translate(0.0f, BODY_OFFSET_Y, WINGS_OFFSET_Z);
|
||||
float pivotY = p.crouching() ? WING_CROUCH_Y : 0.0f;
|
||||
Vec3 left = tip(body, p, WING_PIVOT_X, pivotY, p.rotZ(), p.rotY(), p.rotX(), -TIP_X);
|
||||
Vec3 right = tip(body, p, -WING_PIVOT_X, pivotY, -p.rotZ(), -p.rotY(), p.rotX(), TIP_X);
|
||||
return new Tips(left, right);
|
||||
}
|
||||
|
||||
private static Vec3 tip(Matrix4f body, Pose p, float pivotX, float pivotY, float rz, float ry, float rx,
|
||||
float tipX) {
|
||||
Matrix4f wing = new Matrix4f(body).translate(pivotX, pivotY, 0.0f).rotateZYX(rz, ry, rx);
|
||||
Vector4f v = new Vector4f(tipX, TIP_Y, TIP_Z, 1.0f);
|
||||
wing.transform(v);
|
||||
return new Vec3(p.x() + v.x, p.y() + v.y, p.z() + v.z);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.effects.ribbon;
|
||||
|
||||
/** Точка следа: позиция в мире и время (мс) появления. */
|
||||
public record RibbonPoint(double x, double y, double z, long ms) {
|
||||
/** Точка следа: позиция в мире, время (мс) появления и скорость источника в этот момент. */
|
||||
public record RibbonPoint(double x, double y, double z, long ms, float speed) {
|
||||
|
||||
public RibbonPoint(double x, double y, double z, long ms) {
|
||||
this(x, y, z, ms, 0.0f);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.effects.ribbon;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Форма ленты шлейфа (как у evo ElytraTrails): сужение вдоль следа, реакция на скорость, плавное начало,
|
||||
* затухание у камеры и колебания. Чистые функции, без зависимостей от Minecraft.
|
||||
*/
|
||||
public final class RibbonShape {
|
||||
|
||||
static final float SPEED_MIN = 0.35f;
|
||||
static final float SPEED_MAX = 1.6f;
|
||||
static final float SPEED_WIDTH_FLOOR = 0.45f;
|
||||
static final float SPEED_ALPHA_FLOOR = 0.25f;
|
||||
static final float HEAD_RAMP_POINTS = 3.0f;
|
||||
static final float NEAR_FADE_START = 0.75f;
|
||||
static final float NEAR_FADE_RANGE = 1.5f;
|
||||
private static final float TAU = (float) (Math.PI * 2.0);
|
||||
|
||||
private RibbonShape() {
|
||||
}
|
||||
|
||||
/** Как меняется ширина от головы к хвосту. */
|
||||
public enum Taper {
|
||||
UNIFORM, GROW, SHRINK;
|
||||
|
||||
public static Taper of(String name) {
|
||||
if (name == null) return UNIFORM;
|
||||
return switch (name.toLowerCase(Locale.ROOT)) {
|
||||
case "grow" -> GROW;
|
||||
case "shrink" -> SHRINK;
|
||||
default -> UNIFORM;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Форма пути, в которую «вплетена» лента. */
|
||||
public enum Motion {
|
||||
STRAIGHT, WAVE, HELIX;
|
||||
|
||||
public static Motion of(String name) {
|
||||
if (name == null) return STRAIGHT;
|
||||
return switch (name.toLowerCase(Locale.ROOT)) {
|
||||
case "wave" -> WAVE;
|
||||
case "helix" -> HELIX;
|
||||
default -> STRAIGHT;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Удалённость точки от головы: 0 у самой новой, 1 у самой старой. */
|
||||
public static float tailness(int index, int count) {
|
||||
if (count <= 1) return 0.0f;
|
||||
return 1.0f - index / (float) (count - 1);
|
||||
}
|
||||
|
||||
/** Множитель ширины: у головы 1, у хвоста {@code amount} (Grow) или {@code 1/amount} (Shrink). */
|
||||
public static float taper(Taper mode, float amount, float tailness) {
|
||||
return switch (mode) {
|
||||
case GROW -> 1.0f + tailness * (amount - 1.0f);
|
||||
case SHRINK -> 1.0f + tailness * (1.0f / amount - 1.0f);
|
||||
default -> 1.0f;
|
||||
};
|
||||
}
|
||||
|
||||
/** 0 на медленном планировании, 1 на полной скорости. */
|
||||
public static float speedFactor(float speed) {
|
||||
float v = (speed - SPEED_MIN) / (SPEED_MAX - SPEED_MIN);
|
||||
return v < 0.0f ? 0.0f : Math.min(v, 1.0f);
|
||||
}
|
||||
|
||||
public static float speedWidth(boolean reactive, float speed) {
|
||||
return reactive ? SPEED_WIDTH_FLOOR + (1.0f - SPEED_WIDTH_FLOOR) * speedFactor(speed) : 1.0f;
|
||||
}
|
||||
|
||||
public static float speedAlpha(boolean reactive, float speed) {
|
||||
return reactive ? SPEED_ALPHA_FLOOR + (1.0f - SPEED_ALPHA_FLOOR) * speedFactor(speed) : 1.0f;
|
||||
}
|
||||
|
||||
/** Плавное начало: самые новые точки прозрачны и проявляются за три точки. */
|
||||
public static float headRamp(boolean fadeStart, int index, int count) {
|
||||
if (!fadeStart) return 1.0f;
|
||||
float v = (count - 1 - index) / HEAD_RAMP_POINTS;
|
||||
return v < 0.0f ? 0.0f : Math.min(v, 1.0f);
|
||||
}
|
||||
|
||||
/** Лента гаснет, когда камера вплотную к ней. */
|
||||
public static float nearFade(float distance) {
|
||||
float v = (distance - NEAR_FADE_START) / NEAR_FADE_RANGE;
|
||||
return v < 0.0f ? 0.0f : Math.min(v, 1.0f);
|
||||
}
|
||||
|
||||
/** Оставшаяся жизнь точки, линейно от 1 до 0. */
|
||||
public static float life(long now, long birthMs, long lifeMs) {
|
||||
if (lifeMs <= 0) return 0.0f;
|
||||
float v = 1.0f - (now - birthMs) / (float) lifeMs;
|
||||
return v < 0.0f ? 0.0f : Math.min(v, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Смещение точки от оси пути: {в сторону, по бинормали}. Зависит от времени рождения точки,
|
||||
* поэтому узор едет вместе с точкой, а не пляшет на месте.
|
||||
*/
|
||||
public static float[] motion(Motion mode, long birthMs, float amplitude, float frequency) {
|
||||
if (mode == Motion.STRAIGHT) return new float[]{0.0f, 0.0f};
|
||||
double cycles = (birthMs * 0.001 * frequency) % 1.0;
|
||||
float phase = (float) cycles * TAU;
|
||||
float side = (float) Math.sin(phase) * amplitude;
|
||||
float binormal = mode == Motion.HELIX ? (float) Math.cos(phase) * amplitude : 0.0f;
|
||||
return new float[]{side, binormal};
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,32 @@ public final class TrackBuffer {
|
|||
return added;
|
||||
}
|
||||
|
||||
/** Больше точек лента не держит: самые старые вытесняются. */
|
||||
public static final int MAX_POINTS = 256;
|
||||
|
||||
/**
|
||||
* Как {@link #add}, но для непрерывной ленты (концы крыльев): сдвиг меньше {@code minStep} не записывается,
|
||||
* скачок дальше {@code maxStep} или пауза дольше {@code gapMs} (телепорт, перерыв в полёте) начинают ленту заново,
|
||||
* у каждой точки сохраняется скорость источника.
|
||||
*/
|
||||
public boolean addContinuous(double x, double y, double z, long now, long lifeMs, float speed,
|
||||
double minStep, double maxStep, long gapMs) {
|
||||
RibbonPoint last = points.peekLast();
|
||||
if (last != null && (now - lastSeen > gapMs || distance(last, x, y, z) > maxStep)) {
|
||||
points.clear();
|
||||
last = null;
|
||||
}
|
||||
lastSeen = now;
|
||||
boolean added = false;
|
||||
if (last == null || distance(last, x, y, z) >= minStep) {
|
||||
points.addLast(new RibbonPoint(x, y, z, now, speed));
|
||||
if (points.size() > MAX_POINTS) points.pollFirst();
|
||||
added = true;
|
||||
}
|
||||
trim(now, lifeMs);
|
||||
return added;
|
||||
}
|
||||
|
||||
public void trim(long now, long lifeMs) {
|
||||
while (!points.isEmpty() && now - points.peekFirst().ms() > lifeMs) points.pollFirst();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,57 @@ import java.util.function.IntBinaryOperator;
|
|||
*/
|
||||
public final class TrailRibbon {
|
||||
|
||||
/** Как рисуется лента: ширина и прозрачность по точке, ориентация, мягкие края, ореол и колебания. */
|
||||
public interface Style {
|
||||
/** true: ширина лежит горизонтально (как лента-флаг); false: лента повёрнута к камере. */
|
||||
boolean horizontal();
|
||||
|
||||
/** Края ленты тают в прозрачность вместо плоской полосы. */
|
||||
boolean softEdges();
|
||||
|
||||
/** Масштаб ширины отдельного ореола вокруг ленты; 0 без ореола. */
|
||||
float glowScale();
|
||||
|
||||
/** Полуширина в точке {@code i} из {@code n}. */
|
||||
double halfWidth(float width, int i, int n, RibbonPoint p, float life);
|
||||
|
||||
/** Множитель альфы цвета в точке. */
|
||||
float alpha(int i, int n, RibbonPoint p, float life, float cameraDistance);
|
||||
|
||||
/** Смещение точки: {в сторону, по бинормали}; null без колебаний. */
|
||||
default float[] motion(RibbonPoint p) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Прежний вид (ProjectileTrails): лента к камере, сужение к хвосту, альфа как life в квадрате. */
|
||||
public static final Style LEGACY = new Style() {
|
||||
@Override
|
||||
public boolean horizontal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean softEdges() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float glowScale() {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double halfWidth(float width, int i, int n, RibbonPoint p, float life) {
|
||||
return TrailRibbon.halfWidth(width, life);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float alpha(int i, int n, RibbonPoint p, float life, float cameraDistance) {
|
||||
return life * life;
|
||||
}
|
||||
};
|
||||
|
||||
private TrailRibbon() {
|
||||
}
|
||||
|
||||
|
|
@ -40,18 +91,32 @@ public final class TrailRibbon {
|
|||
return width * 0.5 * (0.25 + 0.75 * l);
|
||||
}
|
||||
|
||||
/** Лента прежнего вида. */
|
||||
public static void render(Renderer3D renderer, Collection<TrackBuffer> tracks, long now, long lifeMs, float width,
|
||||
boolean fill, boolean glow, boolean lines, float lineWidth, boolean depth, Vec3 cam,
|
||||
IntBinaryOperator color) {
|
||||
render(renderer, tracks, now, lifeMs, width, fill, glow, lines, lineWidth, depth, cam, color, LEGACY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param glow вся заливка складывается аддитивно (прежний «Glow»); ореол стиля рисуется отдельным слоем
|
||||
* @param color цвет точки: (индекс от хвоста, число точек) -> ARGB
|
||||
*/
|
||||
public static void render(Renderer3D renderer, Collection<TrackBuffer> tracks, long now, long lifeMs, float width,
|
||||
boolean fill, boolean glow, boolean lines, float lineWidth, boolean depth, Vec3 cam,
|
||||
IntBinaryOperator color) {
|
||||
IntBinaryOperator color, Style style) {
|
||||
var depthMode = depth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN;
|
||||
MeshBuilder fillMesh = null;
|
||||
if (fill) {
|
||||
var pipeline = glow
|
||||
? (depth ? LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH : LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE)
|
||||
: (depth ? LoVisualRenderPipelines.WORLD_COLORED_LIQUID_IGNORE : LoVisualRenderPipelines.WORLD_COLORED);
|
||||
fillMesh = renderer.batch(pipeline, depth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN);
|
||||
fillMesh = renderer.batch(pipeline, depthMode);
|
||||
}
|
||||
MeshBuilder glowMesh = null;
|
||||
if (fill && style.glowScale() > 0.0f) {
|
||||
glowMesh = renderer.batch(depth ? LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH
|
||||
: LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE, depthMode);
|
||||
}
|
||||
MeshBuilder lineMesh = null;
|
||||
if (lines) {
|
||||
|
|
@ -59,41 +124,73 @@ public final class TrailRibbon {
|
|||
try {
|
||||
RenderState.lineWidth = Math.max(0.5f, lineWidth);
|
||||
lineMesh = renderer.batch(depth ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE
|
||||
: LoVisualRenderPipelines.WORLD_COLORED_LINES, depth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN);
|
||||
: LoVisualRenderPipelines.WORLD_COLORED_LINES, depthMode);
|
||||
} finally {
|
||||
RenderState.lineWidth = prev;
|
||||
}
|
||||
}
|
||||
if (fillMesh == null && lineMesh == null) return;
|
||||
if (fillMesh == null && lineMesh == null && glowMesh == null) return;
|
||||
for (TrackBuffer track : tracks) {
|
||||
List<RibbonPoint> pts = track.points();
|
||||
if (pts.size() >= 2) draw(pts, now, lifeMs, width, fillMesh, lineMesh, cam, color);
|
||||
if (pts.size() >= 2) draw(pts, now, lifeMs, width, fillMesh, glowMesh, lineMesh, cam, color, style);
|
||||
}
|
||||
}
|
||||
|
||||
private static void draw(List<RibbonPoint> pts, long now, long lifeMs, float width, MeshBuilder fill,
|
||||
MeshBuilder line, Vec3 cam, IntBinaryOperator color) {
|
||||
MeshBuilder glowMesh, MeshBuilder line, Vec3 cam, IntBinaryOperator color, Style style) {
|
||||
int n = pts.size();
|
||||
double[][] center = new double[n][3];
|
||||
for (int i = 0; i < n; i++) {
|
||||
RibbonPoint p = pts.get(i);
|
||||
center[i] = new double[]{p.x(), p.y(), p.z()};
|
||||
}
|
||||
double[][] side = frame(center, cam, style.horizontal());
|
||||
if (style.motion(pts.get(0)) != null) {
|
||||
double[][] tangent = tangents(center);
|
||||
for (int i = 0; i < n; i++) {
|
||||
float[] m = style.motion(pts.get(i));
|
||||
if (m == null) continue;
|
||||
double bx = side[i][1] * tangent[i][2] - side[i][2] * tangent[i][1];
|
||||
double by = side[i][2] * tangent[i][0] - side[i][0] * tangent[i][2];
|
||||
double bz = side[i][0] * tangent[i][1] - side[i][1] * tangent[i][0];
|
||||
center[i][0] += side[i][0] * m[0] + bx * m[1];
|
||||
center[i][1] += side[i][1] * m[0] + by * m[1];
|
||||
center[i][2] += side[i][2] * m[0] + bz * m[1];
|
||||
}
|
||||
side = frame(center, cam, style.horizontal());
|
||||
}
|
||||
double[][] left = new double[n][3];
|
||||
double[][] right = new double[n][3];
|
||||
int[] argb = new int[n];
|
||||
double[] half = new double[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
RibbonPoint p = pts.get(i);
|
||||
RibbonPoint a = pts.get(Math.max(0, i - 1));
|
||||
RibbonPoint b = pts.get(Math.min(n - 1, i + 1));
|
||||
double dx = b.x() - a.x(), dy = b.y() - a.y(), dz = b.z() - a.z();
|
||||
double vx = cam.x - p.x(), vy = cam.y - p.y(), vz = cam.z - p.z();
|
||||
double sx = dy * vz - dz * vy, sy = dz * vx - dx * vz, sz = dx * vy - dy * vx;
|
||||
double len = Math.sqrt(sx * sx + sy * sy + sz * sz);
|
||||
if (len < 1e-6) len = 1.0;
|
||||
float life = lifeFraction(now, p.ms(), lifeMs);
|
||||
double half = halfWidth(width, life) / len;
|
||||
left[i] = new double[]{p.x() + sx * half, p.y() + sy * half, p.z() + sz * half};
|
||||
right[i] = new double[]{p.x() - sx * half, p.y() - sy * half, p.z() - sz * half};
|
||||
argb[i] = fadeAlpha(color.applyAsInt(i, n), life);
|
||||
float life = style == LEGACY ? lifeFraction(now, p.ms(), lifeMs) : RibbonShape.life(now, p.ms(), lifeMs);
|
||||
half[i] = style.halfWidth(width, i, n, p, life);
|
||||
double dx = center[i][0] - cam.x, dy = center[i][1] - cam.y, dz = center[i][2] - cam.z;
|
||||
float mult = style.alpha(i, n, p, life, (float) Math.sqrt(dx * dx + dy * dy + dz * dz));
|
||||
argb[i] = scaleAlpha(color.applyAsInt(i, n), mult);
|
||||
for (int k = 0; k < 3; k++) {
|
||||
left[i][k] = center[i][k] + side[i][k] * half[i];
|
||||
right[i][k] = center[i][k] - side[i][k] * half[i];
|
||||
}
|
||||
}
|
||||
boolean soft = style.softEdges();
|
||||
for (int i = 1; i < n; i++) {
|
||||
if (fill != null) quad(fill, left[i - 1], right[i - 1], right[i], left[i], argb[i - 1], argb[i]);
|
||||
if (fill != null) {
|
||||
if (soft) softQuad(fill, left[i - 1], center[i - 1], right[i - 1], left[i], center[i], right[i],
|
||||
argb[i - 1], argb[i]);
|
||||
else quad(fill, left[i - 1], right[i - 1], right[i], left[i], argb[i - 1], argb[i]);
|
||||
}
|
||||
if (glowMesh != null) {
|
||||
float g = style.glowScale();
|
||||
double[] l0 = offset(center[i - 1], side[i - 1], half[i - 1] * g);
|
||||
double[] r0 = offset(center[i - 1], side[i - 1], -half[i - 1] * g);
|
||||
double[] l1 = offset(center[i], side[i], half[i] * g);
|
||||
double[] r1 = offset(center[i], side[i], -half[i] * g);
|
||||
softQuad(glowMesh, l0, center[i - 1], r0, l1, center[i], r1,
|
||||
scaleAlpha(argb[i - 1], GLOW_ALPHA), scaleAlpha(argb[i], GLOW_ALPHA));
|
||||
}
|
||||
if (line != null) {
|
||||
segment(line, left[i - 1], left[i], argb[i - 1], argb[i]);
|
||||
segment(line, right[i - 1], right[i], argb[i - 1], argb[i]);
|
||||
|
|
@ -101,15 +198,90 @@ public final class TrailRibbon {
|
|||
}
|
||||
}
|
||||
|
||||
/** Относительная яркость ореола. */
|
||||
static final float GLOW_ALPHA = 0.35f;
|
||||
|
||||
private static double[] offset(double[] c, double[] side, double d) {
|
||||
return new double[]{c[0] + side[0] * d, c[1] + side[1] * d, c[2] + side[2] * d};
|
||||
}
|
||||
|
||||
/** Единичные касательные по центральным разностям соседей. */
|
||||
private static double[][] tangents(double[][] c) {
|
||||
int n = c.length;
|
||||
double[][] out = new double[n][3];
|
||||
for (int i = 0; i < n; i++) {
|
||||
double[] a = c[Math.max(0, i - 1)], b = c[Math.min(n - 1, i + 1)];
|
||||
double dx = b[0] - a[0], dy = b[1] - a[1], dz = b[2] - a[2];
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len < 1e-6) {
|
||||
out[i] = new double[]{0.0, 0.0, 1.0};
|
||||
} else {
|
||||
out[i] = new double[]{dx / len, dy / len, dz / len};
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Единичный вектор «в сторону» для каждой точки: к камере (касательная x взгляд) или горизонтальный
|
||||
* ({@code (-tz, 0, tx)}), с запасным вариантом для вертикального пути.
|
||||
*/
|
||||
static double[][] frame(double[][] c, Vec3 cam, boolean horizontal) {
|
||||
int n = c.length;
|
||||
double[][] t = tangents(c);
|
||||
double[][] out = new double[n][3];
|
||||
for (int i = 0; i < n; i++) {
|
||||
double sx, sy, sz;
|
||||
if (horizontal) {
|
||||
sx = -t[i][2];
|
||||
sy = 0.0;
|
||||
sz = t[i][0];
|
||||
if (sx * sx + sz * sz < 1.0E-7) {
|
||||
sx = 1.0;
|
||||
sz = 0.0;
|
||||
}
|
||||
} else {
|
||||
double vx = cam.x - c[i][0], vy = cam.y - c[i][1], vz = cam.z - c[i][2];
|
||||
sx = t[i][1] * vz - t[i][2] * vy;
|
||||
sy = t[i][2] * vx - t[i][0] * vz;
|
||||
sz = t[i][0] * vy - t[i][1] * vx;
|
||||
}
|
||||
double len = Math.sqrt(sx * sx + sy * sy + sz * sz);
|
||||
if (len < 1e-6) len = 1.0;
|
||||
out[i] = new double[]{sx / len, sy / len, sz / len};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Альфа цвета, умноженная на множитель 0..1; RGB не меняется. */
|
||||
static int scaleAlpha(int argb, float mult) {
|
||||
float m = mult < 0.0f ? 0.0f : Math.min(mult, 1.0f);
|
||||
int alpha = Math.round(((argb >>> 24) & 0xFF) * m);
|
||||
return (alpha << 24) | (argb & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
private static void quad(MeshBuilder m, double[] l0, double[] r0, double[] r1, double[] l1, int c0, int c1) {
|
||||
quad4(m, l0, r0, r1, l1, c0, c0, c1, c1);
|
||||
}
|
||||
|
||||
private static void quad4(MeshBuilder m, double[] p0, double[] p1, double[] p2, double[] p3,
|
||||
int c0, int c1, int c2, int c3) {
|
||||
m.ensureQuadCapacity();
|
||||
int a = m.vec3(l0[0], l0[1], l0[2]).color(new RenderColor(c0)).next();
|
||||
int b = m.vec3(r0[0], r0[1], r0[2]).color(new RenderColor(c0)).next();
|
||||
int c = m.vec3(r1[0], r1[1], r1[2]).color(new RenderColor(c1)).next();
|
||||
int d = m.vec3(l1[0], l1[1], l1[2]).color(new RenderColor(c1)).next();
|
||||
int a = m.vec3(p0[0], p0[1], p0[2]).color(new RenderColor(c0)).next();
|
||||
int b = m.vec3(p1[0], p1[1], p1[2]).color(new RenderColor(c1)).next();
|
||||
int c = m.vec3(p2[0], p2[1], p2[2]).color(new RenderColor(c2)).next();
|
||||
int d = m.vec3(p3[0], p3[1], p3[2]).color(new RenderColor(c3)).next();
|
||||
m.quad(a, b, c, d);
|
||||
}
|
||||
|
||||
/** Две полосы: от прозрачного левого края к непрозрачной оси и от оси к прозрачному правому краю. */
|
||||
private static void softQuad(MeshBuilder m, double[] l0, double[] k0, double[] r0,
|
||||
double[] l1, double[] k1, double[] r1, int c0, int c1) {
|
||||
int e0 = scaleAlpha(c0, 0.0f), e1 = scaleAlpha(c1, 0.0f);
|
||||
quad4(m, l0, k0, k1, l1, e0, c0, c1, e1);
|
||||
quad4(m, k0, r0, r1, k1, c0, e0, e1, c1);
|
||||
}
|
||||
|
||||
private static void segment(MeshBuilder m, double[] p0, double[] p1, int c0, int c1) {
|
||||
m.ensureLineCapacity();
|
||||
int a = m.vec3(p0[0], p0[1], p0[2]).color(new RenderColor(c0)).next();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import dev.loki.lovisual.features.module.core.Module;
|
|||
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||
import dev.loki.lovisual.features.module.phase.HudPhase;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.blizzard.view.BlizzardFog;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.blizzard.view.BlizzardOverlay;
|
||||
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||
import dev.loki.lovisual.render.engine.RenderState;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
|
|
@ -16,10 +18,14 @@ import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
|||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.renderer.fog.FogData;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
|
|
@ -31,28 +37,30 @@ import net.minecraft.world.phys.Vec3;
|
|||
id = "blizzard",
|
||||
displayName = "Blizzard",
|
||||
category = ModuleCategory.VISUALS,
|
||||
description = "Wind-driven snowstorm with white haze and frosted screen edges"
|
||||
description = "High-altitude snowstorm with wind-driven flakes, whiteout fog and frosted edges"
|
||||
)
|
||||
public final class Blizzard extends Module {
|
||||
|
||||
private final NumberValue<Float> intensity = num("blizzardIntensity", "intensity", 0.85f, 0.1f, 1.0f);
|
||||
private final NumberValue<Float> density = num("blizzardDensity", "density", 1.0f, 0.2f, 3.0f);
|
||||
private final NumberValue<Float> speed = num("blizzardSpeed", "speed", 1.0f, 0.3f, 3.0f);
|
||||
private final NumberValue<Float> wind = num("blizzardWind", "wind", 0.7f, 0.0f, 2.0f);
|
||||
private final NumberValue<Float> radius = num("blizzardRadius", "radius", 14.0f, 6.0f, 32.0f);
|
||||
private final NumberValue<Float> flakeSize = num("blizzardFlakeSize", "flake_size", 1.0f, 0.3f, 2.5f);
|
||||
private final NumberValue<Float> opacity = num("blizzardOpacity", "opacity", 0.7f, 0.05f, 1.0f);
|
||||
private final RGBAColorValue tint = color("blizzardTint", "tint", "#F2FFFFFF");
|
||||
private final NumberValue<Float> haze = num("blizzardHaze", "haze", 0.5f, 0.0f, 1.0f);
|
||||
private final NumberValue<Float> wind = num("blizzardWind", "wind", 0.7f, 0.0f, 1.0f);
|
||||
private final NumberValue<Float> turbulence = num("blizzardTurbulence", "turbulence", 0.6f, 0.0f, 1.0f);
|
||||
private final NumberValue<Float> flakeSize = num("blizzardFlakeSize", "flake_size", 1.0f, 0.5f, 2.5f);
|
||||
private final NumberValue<Float> radius = num("blizzardRadius", "radius", 12.0f, 6.0f, 24.0f);
|
||||
private final NumberValue<Float> opacity = num("blizzardOpacity", "opacity", 0.8f, 0.1f, 1.0f);
|
||||
private final BooleanValue whiteout = bool("blizzardWhiteout", "whiteout", true);
|
||||
private final NumberValue<Float> visibility =
|
||||
visibleWhen(num("blizzardVisibility", "visibility", 28.0f, 8.0f, 128.0f), whiteout::get);
|
||||
private final BooleanValue frost = bool("blizzardFrost", "frost", true);
|
||||
private final NumberValue<Float> frostIntensity =
|
||||
visibleWhen(num("blizzardFrostIntensity", "frost_intensity", 0.6f, 0.0f, 1.0f), frost::get);
|
||||
private final BooleanValue onlyWhenSnowing = bool("blizzardOnlyWhenSnowing", "only_when_snowing", false);
|
||||
private final BooleanValue hideVanilla = bool("blizzardHideVanilla", "hide_vanilla", true);
|
||||
private final RGBAColorValue tint = color("blizzardTint", "tint", "#FFFFFFFF");
|
||||
|
||||
private final SnowFlakes flakes = new SnowFlakes();
|
||||
private long lastFrameNanos;
|
||||
private float seconds;
|
||||
private float strength;
|
||||
private float exposure;
|
||||
private boolean frameActive;
|
||||
|
||||
@Override
|
||||
public HudPhase getHudPhase() {
|
||||
|
|
@ -66,68 +74,100 @@ public final class Blizzard extends Module {
|
|||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
lastFrameNanos = 0L;
|
||||
seconds = 0.0f;
|
||||
strength = 0.0f;
|
||||
flakes.clear();
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
reset();
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
lastFrameNanos = 0L;
|
||||
strength = 0.0f;
|
||||
exposure = 0.0f;
|
||||
frameActive = false;
|
||||
flakes.clear();
|
||||
}
|
||||
|
||||
/** Ванильные полосы дождя и снега скрыты, пока идёт шторм и включена настройка. */
|
||||
public boolean hidesVanillaWeather() {
|
||||
return isEnabled() && frameActive && hideVanilla.get();
|
||||
}
|
||||
|
||||
/** Штормовая пелена: сжимает туман и красит его в цвет снега (вызывается из миксина тумана). */
|
||||
public void applyFog(Camera camera, FogData fog) {
|
||||
if (!isEnabled() || !whiteout.get() || camera == null || fog == null) return;
|
||||
FogType type = camera.getFluidInCamera();
|
||||
if (type != FogType.NONE && type != FogType.ATMOSPHERIC) return;
|
||||
float f = strength * exposure;
|
||||
if (!BlizzardFog.active(f)) return;
|
||||
float end = BlizzardFog.endDistance(visibility.get(), f);
|
||||
float start = BlizzardFog.startDistance(visibility.get(), f);
|
||||
fog.environmentalStart = BlizzardFog.limit(fog.environmentalStart, start);
|
||||
fog.environmentalEnd = BlizzardFog.limit(fog.environmentalEnd, end);
|
||||
fog.renderDistanceStart = BlizzardFog.limit(fog.renderDistanceStart, start);
|
||||
fog.renderDistanceEnd = BlizzardFog.limit(fog.renderDistanceEnd, end);
|
||||
fog.skyEnd = BlizzardFog.limit(fog.skyEnd, end * BlizzardFog.SKY_SHARE);
|
||||
fog.cloudEnd = BlizzardFog.limit(fog.cloudEnd, end);
|
||||
int argb = tint.getArgb();
|
||||
float k = BlizzardFog.brightness(fog.color.x, fog.color.y, fog.color.z);
|
||||
fog.color.x = BlizzardFog.blend(fog.color.x, ((argb >> 16) & 0xFF) / 255.0f, k, f);
|
||||
fog.color.y = BlizzardFog.blend(fog.color.y, ((argb >> 8) & 0xFF) / 255.0f, k, f);
|
||||
fog.color.z = BlizzardFog.blend(fog.color.z, (argb & 0xFF) / 255.0f, k, f);
|
||||
}
|
||||
|
||||
private float frameDeltaSeconds() {
|
||||
long now = System.nanoTime();
|
||||
if (lastFrameNanos == 0L) lastFrameNanos = now;
|
||||
float dt = (float) ((now - lastFrameNanos) / 1_000_000_000.0);
|
||||
lastFrameNanos = now;
|
||||
return Mth.clamp(dt, 0.0f, 0.05f);
|
||||
return Mth.clamp(dt, 0.0f, 0.1f);
|
||||
}
|
||||
|
||||
private float targetStrength() {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc.player == null || mc.level == null) return 0.0f;
|
||||
if (!onlyWhenSnowing.get()) return intensity.get();
|
||||
if (mc.level.getRainLevel(1.0f) < 0.02f) return 0.0f;
|
||||
private float targetStrength(Minecraft mc) {
|
||||
float rain = mc.level.getRainLevel(1.0f);
|
||||
BlockPos pos = mc.player.blockPosition();
|
||||
Biome.Precipitation precipitation =
|
||||
mc.level.getBiome(pos).value().getPrecipitationAt(pos, mc.level.getSeaLevel());
|
||||
return precipitation == Biome.Precipitation.SNOW ? intensity.get() : 0.0f;
|
||||
boolean snowBiome = mc.level.getBiome(pos).value()
|
||||
.getPrecipitationAt(pos, mc.level.getSeaLevel()) == Biome.Precipitation.SNOW;
|
||||
return SnowFlakes.targetStrength(intensity.get(), onlyWhenSnowing.get(), rain, snowBiome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (!isEnabled() || mc.player == null || mc.level == null || mc.gameRenderer == null) return;
|
||||
if (mc.gameRenderer.mainCamera() == null) return;
|
||||
float dt = frameDeltaSeconds();
|
||||
seconds += dt;
|
||||
strength = SnowFlakes.ramp(strength, targetStrength(), dt);
|
||||
float norm = strength;
|
||||
if (norm <= 0.001f) {
|
||||
flakes.clear();
|
||||
if (!isEnabled() || mc.player == null || mc.level == null || mc.gameRenderer == null
|
||||
|| mc.gameRenderer.mainCamera() == null) {
|
||||
frameActive = false;
|
||||
return;
|
||||
}
|
||||
Camera camera = mc.gameRenderer.mainCamera();
|
||||
Vec3 cam = camera.position();
|
||||
SnowFlakes.Wind windValue = SnowFlakes.wind(seconds, wind.get(), norm);
|
||||
int target = SnowFlakes.targetCount(radius.get(), density.get(), norm);
|
||||
flakes.update(dt, cam.x, cam.y, cam.z, radius.get(), windValue,
|
||||
SnowFlakes.turbulence(wind.get()), speed.get(), target, seconds);
|
||||
float dt = frameDeltaSeconds();
|
||||
Vec3 cam = mc.gameRenderer.mainCamera().position();
|
||||
ClientLevel level = mc.level;
|
||||
SnowFlakes.Ground ground = (x, z) -> level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z);
|
||||
strength = SnowFlakes.ramp(strength, targetStrength(mc), dt);
|
||||
float open = SnowFlakes.exposureTarget(cam.y, ground.height(Mth.floor(cam.x), Mth.floor(cam.z)));
|
||||
exposure = SnowFlakes.approach(exposure, open, SnowFlakes.EXPOSURE_PER_SECOND, dt);
|
||||
frameActive = strength > 0.01f;
|
||||
if (!frameActive && flakes.count == 0) return;
|
||||
|
||||
double seconds = (System.currentTimeMillis() % 600_000L) / 1000.0;
|
||||
SnowFlakes.Wind windValue = SnowFlakes.wind(seconds, wind.get(), strength);
|
||||
int target = frameActive ? SnowFlakes.targetCount(radius.get(), density.get(), strength) : 0;
|
||||
flakes.update(dt, cam.x, cam.y, cam.z, radius.get(), windValue, SnowFlakes.turbulence(turbulence.get()),
|
||||
target, seconds, ground);
|
||||
if (flakes.count == 0) return;
|
||||
MeshBuilder mesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_DEPTH, Renderer3D.DepthMode.MAIN);
|
||||
if (mesh == null) return;
|
||||
BlizzardRenderer.render(mesh, flakes, RenderState.cameraRotation,
|
||||
cam.x, cam.y, cam.z, radius.get(), tint.getArgb(), norm, flakeSize.get(), opacity.get());
|
||||
BlizzardRenderer.render(mesh, flakes, RenderState.cameraRotation, cam.x, cam.y, cam.z,
|
||||
tint.getArgb(), strength, intensity.get(), flakeSize.get(), opacity.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer,
|
||||
GuiGraphicsExtractor ctx, float tickDelta) {
|
||||
if (!isEnabled()) return;
|
||||
float norm = strength;
|
||||
if (norm <= 0.01f) return;
|
||||
BlizzardOverlay.render(ctx, norm, haze.get(), frost.get(), frostIntensity.get(), tint.getArgb());
|
||||
float current = strength * exposure * intensity.get();
|
||||
BlizzardOverlay.render(ctx, current, frost.get(), tint.getArgb());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.weather.blizzard;
|
||||
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
|
||||
/**
|
||||
* Белая дымка и обмерзание краёв экрана: слой из полупрозрачных рамок,
|
||||
* каждая следующая тоньше, поэтому альфа копится к самому краю.
|
||||
*/
|
||||
final class BlizzardOverlay {
|
||||
|
||||
static final int FROST_BANDS = 16;
|
||||
static final float MAX_VEIL = 0.85f;
|
||||
static final float VEIL_SCALE = 0.55f;
|
||||
static final float FROST_TOTAL = 0.6f;
|
||||
|
||||
private BlizzardOverlay() {
|
||||
}
|
||||
|
||||
static void render(GuiGraphicsExtractor ctx, float strength, float haze,
|
||||
boolean frost, float frostStrength, int tintArgb) {
|
||||
if (ctx == null || strength <= 0.01f) return;
|
||||
int w = ctx.guiWidth();
|
||||
int h = ctx.guiHeight();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
float veil = veilAlpha(haze, strength);
|
||||
if (veil > 0.004f) ctx.fill(0, 0, w, h, withAlpha(tintArgb, veil));
|
||||
if (!frost || frostStrength <= 0.01f) return;
|
||||
float total = frostTotal(strength, frostStrength);
|
||||
if (total <= 0.004f) return;
|
||||
int color = withAlpha(tintArgb, frostBandAlpha(total));
|
||||
int depth = Math.max(2, Math.round(Math.min(w, h) * 0.20f));
|
||||
for (int i = 0; i < FROST_BANDS; i++) {
|
||||
int thickness = bandThickness(i, FROST_BANDS, depth);
|
||||
ctx.fill(0, 0, w, thickness, color);
|
||||
ctx.fill(0, h - thickness, w, h, color);
|
||||
ctx.fill(0, thickness, thickness, h - thickness, color);
|
||||
ctx.fill(w - thickness, thickness, w, h - thickness, color);
|
||||
}
|
||||
}
|
||||
|
||||
/** Непрозрачность белой дымки на весь экран: растёт с дымкой и силой шторма, ограничена {@link #MAX_VEIL}. */
|
||||
static float veilAlpha(float haze, float strength) {
|
||||
return Math.max(0.0f, Math.min(MAX_VEIL, haze * strength * VEIL_SCALE));
|
||||
}
|
||||
|
||||
/** Суммарная непрозрачность обмерзания у самого края. */
|
||||
static float frostTotal(float strength, float frostStrength) {
|
||||
return Math.max(0.0f, FROST_TOTAL * strength * frostStrength);
|
||||
}
|
||||
|
||||
/** Непрозрачность одной полосы обмерзания: все полосы вместе дают {@code total}. */
|
||||
static float frostBandAlpha(float total) {
|
||||
return total / FROST_BANDS;
|
||||
}
|
||||
|
||||
/** Толщина i-й полосы: первая на всю глубину, каждая следующая тоньше, поэтому альфа копится у края. */
|
||||
static int bandThickness(int i, int bands, int depth) {
|
||||
return Math.max(1, Math.round(depth * (1.0f - (float) Math.sqrt(i / (double) bands))));
|
||||
}
|
||||
|
||||
static int withAlpha(int argb, float alpha) {
|
||||
int a = Math.round(alpha * 255.0f);
|
||||
if (a < 0) a = 0;
|
||||
if (a > 255) a = 255;
|
||||
return (a << 24) | (argb & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
/** Доля полос обмерзания, покрывающая точку на distance от края экрана. */
|
||||
static float edgeCoverage(float distance, float depth, int bands) {
|
||||
if (bands <= 0 || depth <= 0.0f) return 0.0f;
|
||||
if (distance <= 0.0f) return 1.0f;
|
||||
int covered = 0;
|
||||
for (int i = 0; i < bands; i++) {
|
||||
float inset = depth * (1.0f - (float) Math.sqrt(i / (double) bands));
|
||||
if (distance < inset) covered++;
|
||||
}
|
||||
return covered / (float) bands;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,114 +6,127 @@ import org.joml.Quaternionf;
|
|||
import org.joml.Vector3f;
|
||||
|
||||
/**
|
||||
* Рендер снежинок: биллборд-квады, вытянутые по вектору скорости частицы,
|
||||
* с затуханием у камеры и на границе зоны.
|
||||
* Рендер снежинок по образцу evo: мягкая «капля» из двух воздушных змеев, вытянутая вдоль скорости,
|
||||
* непрозрачный центр и прозрачные края. Снежинки позади камеры отбрасываются.
|
||||
*/
|
||||
final class BlizzardRenderer {
|
||||
|
||||
static final float FLAKE_RADIUS = 0.028f;
|
||||
static final float MIN_ANGULAR_RADIUS = 0.0024f;
|
||||
static final float STREAK_TIME = 0.022f;
|
||||
static final float STREAK_FRONT_SHARE = 0.3f;
|
||||
static final float FADE_IN_SECONDS = 0.45f;
|
||||
static final float NEAR_FADE_START = 0.25f;
|
||||
static final float NEAR_FADE_SPAN = 0.65f;
|
||||
static final float BEHIND_DOT = -0.35f;
|
||||
static final float BEHIND_MIN_DISTANCE = 3.0f;
|
||||
|
||||
private BlizzardRenderer() {
|
||||
}
|
||||
|
||||
static void render(MeshBuilder mesh, SnowFlakes flakes, Quaternionf camRot,
|
||||
double camX, double camY, double camZ, float radius,
|
||||
int argb, float strength, float sizeMul, float opacity) {
|
||||
static void render(MeshBuilder mesh, SnowFlakes flakes, Quaternionf camRot, double camX, double camY, double camZ,
|
||||
int argb, float strength, float intensity, float sizeSetting, float opacity) {
|
||||
if (mesh == null || flakes.count == 0 || strength <= 0.001f || opacity <= 0.004f) return;
|
||||
int r = (argb >>> 16) & 0xFF;
|
||||
int g = (argb >>> 8) & 0xFF;
|
||||
int b = argb & 0xFF;
|
||||
Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot);
|
||||
Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot);
|
||||
int rgb = argb & 0x00FFFFFF;
|
||||
Vector3f forward = new Vector3f(0.0f, 0.0f, -1.0f).rotate(camRot);
|
||||
Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot);
|
||||
for (int i = 0; i < flakes.count; i++) {
|
||||
double dx = flakes.x[i] - camX;
|
||||
double dy = flakes.y[i] - camY;
|
||||
double dz = flakes.z[i] - camZ;
|
||||
float dist = (float) Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (dist < 0.2f) continue;
|
||||
float facing = (float) (dx * forward.x + dy * forward.y + dz * forward.z);
|
||||
if (facing <= 0.0f) continue;
|
||||
|
||||
float speedX = flakes.vx[i];
|
||||
float speedY = flakes.vy[i];
|
||||
float speedZ = flakes.vz[i];
|
||||
float along = right.x * speedX + right.y * speedY + right.z * speedZ;
|
||||
float across = up.x * speedX + up.y * speedY + up.z * speedZ;
|
||||
float alongLen = (float) Math.sqrt(along * along + across * across);
|
||||
if (alongLen < 1.0E-4f) {
|
||||
along = 0.0f;
|
||||
across = -1.0f;
|
||||
alongLen = 1.0f;
|
||||
}
|
||||
along /= alongLen;
|
||||
across /= alongLen;
|
||||
// Единичный вектор скорости в плоскости биллборда.
|
||||
float dirX = right.x * along + up.x * across;
|
||||
float dirY = right.y * along + up.y * across;
|
||||
float dirZ = right.z * along + up.z * across;
|
||||
// Поперёк: forward x dir (оба перпендикулярны друг другу).
|
||||
float sideX = forward.y * dirZ - forward.z * dirY;
|
||||
float sideY = forward.z * dirX - forward.x * dirZ;
|
||||
float sideZ = forward.x * dirY - forward.y * dirX;
|
||||
float sideLen = (float) Math.sqrt(sideX * sideX + sideY * sideY + sideZ * sideZ);
|
||||
if (sideLen < 1.0E-4f) continue;
|
||||
sideX /= sideLen;
|
||||
sideY /= sideLen;
|
||||
sideZ /= sideLen;
|
||||
|
||||
float speed = (float) Math.sqrt(speedX * speedX + speedY * speedY + speedZ * speedZ);
|
||||
float halfWidth = halfWidth(flakes.size[i], sizeMul, dist);
|
||||
float halfLength = halfLength(halfWidth, speed, sizeMul);
|
||||
float alpha = flakeAlpha(dist, flakes.age[i], flakes.alpha[i], radius, opacity, strength);
|
||||
float px = (float) (flakes.x[i] - camX);
|
||||
float py = (float) (flakes.y[i] - camY);
|
||||
float pz = (float) (flakes.z[i] - camZ);
|
||||
float dist = (float) Math.sqrt(px * px + py * py + pz * pz);
|
||||
if (dist <= 0.0f) continue;
|
||||
float vx = px / dist, vy = py / dist, vz = pz / dist;
|
||||
if (isBehind(vx * forward.x + vy * forward.y + vz * forward.z, dist)) continue;
|
||||
float alpha = flakeAlpha(dist, flakes.age[i], flakes.alpha[i], opacity, strength, intensity);
|
||||
if (alpha <= 0.0f) continue;
|
||||
|
||||
float cx = (float) flakes.x[i];
|
||||
float cy = (float) flakes.y[i];
|
||||
float cz = (float) flakes.z[i];
|
||||
float ax = dirX * halfLength;
|
||||
float ay = dirY * halfLength;
|
||||
float az = dirZ * halfLength;
|
||||
float bx = sideX * halfWidth;
|
||||
float by = sideY * halfWidth;
|
||||
float bz = sideZ * halfWidth;
|
||||
RenderColor color = new RenderColor(((int) (alpha * 255.0f) << 24) | (r << 16) | (g << 8) | b);
|
||||
mesh.ensureQuadCapacity();
|
||||
int i1 = mesh.vec3(cx - ax - bx, cy - ay - by, cz - az - bz).color(color).next();
|
||||
int i2 = mesh.vec3(cx + ax - bx, cy + ay - by, cz + az - bz).color(color).next();
|
||||
int i3 = mesh.vec3(cx + ax + bx, cy + ay + by, cz + az + bz).color(color).next();
|
||||
int i4 = mesh.vec3(cx - ax + bx, cy - ay + by, cz - az + bz).color(color).next();
|
||||
mesh.quad(i1, i2, i3, i4);
|
||||
float speed = (float) Math.sqrt(flakes.vx[i] * flakes.vx[i] + flakes.vy[i] * flakes.vy[i]
|
||||
+ flakes.vz[i] * flakes.vz[i]);
|
||||
float dx, dy, dz;
|
||||
if (speed <= 1.0E-4f) {
|
||||
dx = up.x;
|
||||
dy = up.y;
|
||||
dz = up.z;
|
||||
} else {
|
||||
dx = flakes.vx[i] / speed;
|
||||
dy = flakes.vy[i] / speed;
|
||||
dz = flakes.vz[i] / speed;
|
||||
}
|
||||
float sx = dy * vz - dz * vy, sy = dz * vx - dx * vz, sz = dx * vy - dy * vx;
|
||||
float sideLen = (float) Math.sqrt(sx * sx + sy * sy + sz * sz);
|
||||
if (sideLen <= 1.0E-4f) {
|
||||
sx = up.y * vz - up.z * vy;
|
||||
sy = up.z * vx - up.x * vz;
|
||||
sz = up.x * vy - up.y * vx;
|
||||
sideLen = (float) Math.sqrt(sx * sx + sy * sy + sz * sz);
|
||||
if (sideLen <= 1.0E-4f) continue;
|
||||
}
|
||||
sx /= sideLen;
|
||||
sy /= sideLen;
|
||||
sz /= sideLen;
|
||||
|
||||
float radius = radius(sizeSetting, flakes.size[i], dist);
|
||||
float back = streakLength(radius, speed, flakes.size[i]);
|
||||
float front = frontLength(radius, back);
|
||||
RenderColor center = new RenderColor(withAlpha(rgb, alpha));
|
||||
RenderColor edge = new RenderColor(withAlpha(rgb, 0.0f));
|
||||
emit(mesh, px, py, pz, sx * radius, sy * radius, sz * radius, dx * front, dy * front, dz * front,
|
||||
center, edge);
|
||||
emit(mesh, px, py, pz, -sx * radius, -sy * radius, -sz * radius, -dx * back, -dy * back, -dz * back,
|
||||
center, edge);
|
||||
}
|
||||
}
|
||||
|
||||
/** Один змей: центр непрозрачный, сторона, острие и противоположная сторона прозрачные. */
|
||||
private static void emit(MeshBuilder mesh, float cx, float cy, float cz, float sx, float sy, float sz,
|
||||
float tx, float ty, float tz, RenderColor center, RenderColor edge) {
|
||||
mesh.ensureQuadCapacity();
|
||||
int a = mesh.vec3(cx, cy, cz).color(center).next();
|
||||
int b = mesh.vec3(cx - sx, cy - sy, cz - sz).color(edge).next();
|
||||
int c = mesh.vec3(cx + tx, cy + ty, cz + tz).color(edge).next();
|
||||
int d = mesh.vec3(cx + sx, cy + sy, cz + sz).color(edge).next();
|
||||
mesh.quad(a, b, c, d);
|
||||
}
|
||||
|
||||
/** Снежинки позади камеры не рисуются, кроме совсем близких (не пропадают на краю экрана). */
|
||||
static boolean isBehind(float dotWithForward, float dist) {
|
||||
return dist > BEHIND_MIN_DISTANCE && dotWithForward < BEHIND_DOT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Видимость снежинки: невидима у самой линзы, плавно проявляется с расстоянием и возрастом,
|
||||
* тает к границе зоны шторма; масштабируется прозрачностью и силой шторма, не бывает полностью непрозрачной.
|
||||
* Слишком слабые снежинки возвращают 0, чтобы их не рисовать.
|
||||
* Видимость снежинки: невидима вплотную к камере, плавно проявляется с расстоянием и возрастом;
|
||||
* масштабируется прозрачностью и долей силы шторма от интенсивности.
|
||||
*/
|
||||
static float flakeAlpha(float dist, float ageSeconds, float baseAlpha, float radius, float opacity, float strength) {
|
||||
float edgeStart = radius * 0.62f;
|
||||
float edgeSpan = Math.max(0.001f, radius * 1.05f - edgeStart);
|
||||
float edge = 1.0f - clamp01((dist - edgeStart) / edgeSpan);
|
||||
float near = clamp01((dist - 0.35f) / 0.5f);
|
||||
float ageFade = Math.min(1.0f, ageSeconds / 0.45f);
|
||||
float alpha = opacity * strength * baseAlpha * edge * near * ageFade;
|
||||
if (alpha <= 0.008f) return 0.0f;
|
||||
return Math.min(0.95f, alpha);
|
||||
static float flakeAlpha(float dist, float ageSeconds, float baseAlpha, float opacity, float strength,
|
||||
float intensity) {
|
||||
float near = clamp01((dist - NEAR_FADE_START) / NEAR_FADE_SPAN);
|
||||
float born = Math.min(ageSeconds / FADE_IN_SECONDS, 1.0f);
|
||||
float share = clamp01(strength / Math.max(intensity, 0.01f));
|
||||
return opacity * share * baseAlpha * near * Math.max(0.0f, born);
|
||||
}
|
||||
|
||||
/** Полуширина снежинки; вдали не тоньше доли пикселя, чтобы не пропадала. */
|
||||
static float halfWidth(float flakeSize, float sizeMul, float dist) {
|
||||
float hw = (float) (0.045 * flakeSize) * sizeMul;
|
||||
return Math.max(hw, dist * 0.0016f);
|
||||
/** Радиус снежинки: вдали не меньше угловых 0.0024, чтобы не пропадала. */
|
||||
static float radius(float sizeSetting, float flakeSize, float dist) {
|
||||
return Math.max(FLAKE_RADIUS * sizeSetting * flakeSize, dist * MIN_ANGULAR_RADIUS);
|
||||
}
|
||||
|
||||
/** Полудлина штриха: быстрее снежинка, длиннее след, но не больше 0.35 блока. */
|
||||
static float halfLength(float halfWidth, float speed, float sizeMul) {
|
||||
return halfWidth + Math.min(0.35f, speed * 0.022f * sizeMul);
|
||||
/** Длина хвоста за снежинкой: растёт со скоростью. */
|
||||
static float streakLength(float radius, float speed, float flakeSize) {
|
||||
return radius + speed * STREAK_TIME * flakeSize;
|
||||
}
|
||||
|
||||
/** Длина носа перед снежинкой: короче хвоста, у медленной равна радиусу. */
|
||||
static float frontLength(float radius, float back) {
|
||||
return back * STREAK_FRONT_SHARE + radius * (1.0f - STREAK_FRONT_SHARE);
|
||||
}
|
||||
|
||||
static int withAlpha(int rgb, float alpha) {
|
||||
int a = Math.max(0, Math.min(255, Math.round(alpha * 255.0f)));
|
||||
return (a << 24) | (rgb & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
private static float clamp01(float v) {
|
||||
if (v < 0.0f) return 0.0f;
|
||||
return v > 1.0f ? 1.0f : v;
|
||||
return v < 0.0f ? 0.0f : Math.min(v, 1.0f);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,33 @@ package dev.loki.lovisual.features.module.modules.visuals.weather.blizzard;
|
|||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Чистая физика снежинок: спавн в цилиндре вокруг камеры, ветер с порывами,
|
||||
* турбулентность и рецикл при выходе из зоны. Без зависимостей от Minecraft.
|
||||
* Чистая физика снежинок по образцу evo: спавн в цилиндре вокруг камеры между землёй (карта высот) и потолком
|
||||
* над головой, ветер с порывами, турбулентность, рецикл при выходе из зоны или касании земли.
|
||||
* Под крышей земля выше потолка спавна, поэтому снега внутри помещений нет. Без зависимостей от Minecraft.
|
||||
*/
|
||||
final class SnowFlakes {
|
||||
|
||||
static final int MAX_FLAKES = 4000;
|
||||
static final int MAX_FLAKES = 12000;
|
||||
static final float FALL_SPEED = 1.6f;
|
||||
static final float AREA_FACTOR = 3.0f;
|
||||
static final double SPAWN_ABOVE = 8.0;
|
||||
static final double SPAWN_BELOW = 6.0;
|
||||
static final float AREA_FACTOR = 9.0f;
|
||||
static final double SPAWN_CEILING = 8.0;
|
||||
static final double SPAWN_FLOOR_MARGIN = 6.0;
|
||||
static final double RECYCLE_MARGIN = 1.25;
|
||||
static final double RECYCLE_HEIGHT = 16.0;
|
||||
static final int SPAWN_ATTEMPTS = 3;
|
||||
static final float WIND_SPEED_MAX = 16.0f;
|
||||
static final float TURBULENCE_AMPLITUDE = 1.1f;
|
||||
static final float RISE_PER_SECOND = 0.45f;
|
||||
static final float FALL_PER_SECOND = 0.6f;
|
||||
static final float EXPOSURE_PER_SECOND = 1.4f;
|
||||
|
||||
/** Горизонтальный ветер с лёгким вертикальным сносом. */
|
||||
/** Высота верхнего блока, который останавливает снег, в колонке (x, z) (карта MOTION_BLOCKING). */
|
||||
@FunctionalInterface
|
||||
interface Ground {
|
||||
float height(int x, int z);
|
||||
}
|
||||
|
||||
/** Ветер: горизонтальная скорость и лёгкий вертикальный снос. */
|
||||
record Wind(double x, double y, double z) {
|
||||
static final Wind ZERO = new Wind(0.0, 0.0, 0.0);
|
||||
}
|
||||
|
|
@ -32,6 +46,9 @@ final class SnowFlakes {
|
|||
final float[] phase = new float[MAX_FLAKES];
|
||||
final float[] rate = new float[MAX_FLAKES];
|
||||
final float[] age = new float[MAX_FLAKES];
|
||||
private final float[] ground = new float[MAX_FLAKES];
|
||||
private final int[] columnX = new int[MAX_FLAKES];
|
||||
private final int[] columnZ = new int[MAX_FLAKES];
|
||||
int count;
|
||||
|
||||
private final Random random = new Random(1L);
|
||||
|
|
@ -40,80 +57,150 @@ final class SnowFlakes {
|
|||
count = 0;
|
||||
}
|
||||
|
||||
/** Фиксация seed — детерминизм для тестов. */
|
||||
/** Фиксация seed: детерминизм для тестов. */
|
||||
void reseed(long seed) {
|
||||
random.setSeed(seed);
|
||||
}
|
||||
|
||||
/** Плавный разгон (0.45/с) и затухание (0.6/с) силы шторма к цели, без перелёта. */
|
||||
static float ramp(float current, float target, float dt) {
|
||||
if (current < target) return Math.min(target, current + 0.45f * dt);
|
||||
return Math.max(target, current - 0.6f * dt);
|
||||
/** Шаг к цели с постоянной скоростью, без перелёта. */
|
||||
static float approach(float current, float target, float perSecond, float dt) {
|
||||
float step = perSecond * dt;
|
||||
if (Math.abs(target - current) <= step) return target;
|
||||
return current + (target > current ? step : -step);
|
||||
}
|
||||
|
||||
/** Целевое число частиц: площадь цилиндра * плотность * доля силы шторма. */
|
||||
/** Сила шторма: разгон 0.45/с, затухание 0.6/с. */
|
||||
static float ramp(float current, float target, float dt) {
|
||||
return approach(current, target, target > current ? RISE_PER_SECOND : FALL_PER_SECOND, dt);
|
||||
}
|
||||
|
||||
/** Цель экспозиции: 1 на открытом небе (камера не ниже верхнего блока колонки), 0 под крышей. */
|
||||
static float exposureTarget(double camY, float groundHeight) {
|
||||
return camY < groundHeight ? 0.0f : 1.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Целевая сила шторма. Без «только в снегопад» это просто интенсивность; иначе шторм идёт, пока в биоме
|
||||
* игрока снег и идёт осадок, и растёт с его силой.
|
||||
*/
|
||||
static float targetStrength(float intensity, boolean onlyInSnowfall, float rainLevel, boolean snowBiome) {
|
||||
if (!onlyInSnowfall) return intensity;
|
||||
if (rainLevel <= 0.02f || !snowBiome) return 0.0f;
|
||||
return Math.max(0.0f, Math.min(1.0f, rainLevel)) * intensity;
|
||||
}
|
||||
|
||||
/** Целевое число частиц: площадь цилиндра * плотность * сила шторма, не больше {@link #MAX_FLAKES}. */
|
||||
static int targetCount(float radius, float density, float strength) {
|
||||
if (radius <= 0f || density <= 0f || strength <= 0f) return 0;
|
||||
double raw = Math.PI * radius * radius * AREA_FACTOR * density * Math.min(1.0f, strength);
|
||||
if (raw >= MAX_FLAKES) return MAX_FLAKES;
|
||||
return (int) raw;
|
||||
double raw = Math.PI * radius * radius * AREA_FACTOR * density * strength;
|
||||
return (int) Math.min(MAX_FLAKES, raw);
|
||||
}
|
||||
|
||||
/** Ветер с порывами: направление медленно кружит, скорость дышит несколькими синусами. */
|
||||
static Wind wind(float seconds, float windPower, float strength) {
|
||||
/** Ветер с порывами: направление медленно поворачивает, скорость дышит двумя синусами. */
|
||||
static Wind wind(double seconds, float windPower, float strength) {
|
||||
if (strength <= 0f || windPower <= 0f) return Wind.ZERO;
|
||||
double angle = seconds * 0.17 + Math.sin(seconds * 0.45) * 0.35;
|
||||
double angle = seconds * 0.02 + Math.sin(seconds * 0.9 * 0.5) * 0.35;
|
||||
double gust = 1.0 + Math.sin(seconds * 0.9) * 0.35 + Math.sin(seconds * 2.3) * 0.15;
|
||||
double speed = windPower * 14.0 * gust * strength;
|
||||
double speed = windPower * WIND_SPEED_MAX * gust * strength;
|
||||
return new Wind(Math.cos(angle) * speed, Math.sin(seconds * 2.3) * speed * 0.12, Math.sin(angle) * speed);
|
||||
}
|
||||
|
||||
/** Амплитуда боковой турбулентности, зависящей от мощности ветра. */
|
||||
static float turbulence(float windPower) {
|
||||
return 0.25f + 0.55f * Math.max(0f, windPower);
|
||||
/** Амплитуда боковой турбулентности (блоков/с) по настройке 0..1. */
|
||||
static float turbulence(float setting) {
|
||||
return Math.max(0f, setting) * TURBULENCE_AMPLITUDE;
|
||||
}
|
||||
|
||||
void update(float dt, double camX, double camY, double camZ, float radius,
|
||||
Wind wind, float turbulence, float speedMul, int target, float seconds) {
|
||||
void update(float dt, double camX, double camY, double camZ, float radius, Wind wind, float turbulence,
|
||||
int target, double seconds, Ground ground) {
|
||||
if (dt <= 0f || target < 0) return;
|
||||
target = Math.min(target, MAX_FLAKES);
|
||||
if (count > target) count = target;
|
||||
while (count < target) spawn(count++, camX, camY, camZ, radius);
|
||||
while (count < target && respawn(count, camX, camY, camZ, radius, wind, ground)) count++;
|
||||
double recycle = radius * RECYCLE_MARGIN;
|
||||
double recycleSq = recycle * recycle;
|
||||
for (int i = 0; i < count; i++) {
|
||||
float wobbleX = (float) Math.sin(seconds * rate[i] + phase[i]) * turbulence * speedMul;
|
||||
float wobbleZ = (float) Math.cos(seconds * rate[i] * 0.83f + phase[i] * 1.7f) * turbulence * speedMul;
|
||||
vx[i] = (float) wind.x() + wobbleX;
|
||||
vy[i] = (float) wind.y() - FALL_SPEED * fall[i] * speedMul;
|
||||
vz[i] = (float) wind.z() + wobbleZ;
|
||||
x[i] += vx[i] * dt;
|
||||
y[i] += vy[i] * dt;
|
||||
z[i] += vz[i] * dt;
|
||||
age[i] += dt;
|
||||
double dx = x[i] - camX;
|
||||
double dz = z[i] - camZ;
|
||||
if (dx * dx + dz * dz > recycleSq
|
||||
|| y[i] < camY - SPAWN_BELOW
|
||||
|| y[i] > camY + SPAWN_ABOVE) {
|
||||
spawn(i, camX, camY, camZ, radius);
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
float wobbleX = (float) Math.sin(seconds * rate[i] + phase[i]) * turbulence;
|
||||
float wobbleZ = (float) Math.cos(seconds * rate[i] * 0.83f + phase[i] * 1.7f) * turbulence;
|
||||
float nvx = (float) wind.x() + wobbleX;
|
||||
float nvy = (float) wind.y() - FALL_SPEED * fall[i];
|
||||
float nvz = (float) wind.z() + wobbleZ;
|
||||
double nx = x[i] + nvx * dt;
|
||||
double ny = y[i] + nvy * dt;
|
||||
double nz = z[i] + nvz * dt;
|
||||
int cx = (int) Math.floor(nx);
|
||||
int cz = (int) Math.floor(nz);
|
||||
if (cx != columnX[i] || cz != columnZ[i]) {
|
||||
columnX[i] = cx;
|
||||
columnZ[i] = cz;
|
||||
this.ground[i] = ground.height(cx, cz);
|
||||
}
|
||||
double dx = nx - camX;
|
||||
double dz = nz - camZ;
|
||||
boolean lost = ny <= this.ground[i] || dx * dx + dz * dz > recycleSq || ny > camY + RECYCLE_HEIGHT;
|
||||
if (lost) {
|
||||
if (!respawn(i, camX, camY, camZ, radius, wind, ground)) remove(i);
|
||||
continue;
|
||||
}
|
||||
x[i] = nx;
|
||||
y[i] = ny;
|
||||
z[i] = nz;
|
||||
vx[i] = nvx;
|
||||
vy[i] = nvy;
|
||||
vz[i] = nvz;
|
||||
age[i] += dt;
|
||||
}
|
||||
}
|
||||
|
||||
private void spawn(int i, double camX, double camY, double camZ, float radius) {
|
||||
double angle = random.nextDouble() * Math.PI * 2.0;
|
||||
double dist = radius * Math.sqrt(random.nextDouble());
|
||||
x[i] = camX + Math.cos(angle) * dist;
|
||||
z[i] = camZ + Math.sin(angle) * dist;
|
||||
y[i] = camY + SPAWN_ABOVE - random.nextDouble() * (SPAWN_ABOVE + SPAWN_BELOW);
|
||||
vx[i] = 0f;
|
||||
vy[i] = -FALL_SPEED;
|
||||
vz[i] = 0f;
|
||||
fall[i] = 0.6f + random.nextFloat() * 0.8f;
|
||||
size[i] = 0.75f + random.nextFloat() * 0.55f;
|
||||
alpha[i] = 0.35f + random.nextFloat() * 0.55f;
|
||||
phase[i] = random.nextFloat() * (float) (Math.PI * 2.0);
|
||||
rate[i] = 1.2f + random.nextFloat() * 2.2f;
|
||||
age[i] = 0f;
|
||||
private void remove(int i) {
|
||||
int last = --count;
|
||||
if (i == last) return;
|
||||
x[i] = x[last];
|
||||
y[i] = y[last];
|
||||
z[i] = z[last];
|
||||
vx[i] = vx[last];
|
||||
vy[i] = vy[last];
|
||||
vz[i] = vz[last];
|
||||
fall[i] = fall[last];
|
||||
size[i] = size[last];
|
||||
alpha[i] = alpha[last];
|
||||
phase[i] = phase[last];
|
||||
rate[i] = rate[last];
|
||||
age[i] = age[last];
|
||||
ground[i] = ground[last];
|
||||
columnX[i] = columnX[last];
|
||||
columnZ[i] = columnZ[last];
|
||||
}
|
||||
|
||||
/** Ставит снежинку в случайное место цилиндра между землёй и потолком; false, если над колонкой крыша. */
|
||||
private boolean respawn(int i, double camX, double camY, double camZ, float radius, Wind wind, Ground world) {
|
||||
for (int attempt = 0; attempt < SPAWN_ATTEMPTS; attempt++) {
|
||||
double angle = random.nextDouble() * Math.PI * 2.0;
|
||||
double dist = radius * Math.sqrt(random.nextDouble());
|
||||
double px = camX + Math.cos(angle) * dist;
|
||||
double pz = camZ + Math.sin(angle) * dist;
|
||||
int cx = (int) Math.floor(px);
|
||||
int cz = (int) Math.floor(pz);
|
||||
float height = world.height(cx, cz);
|
||||
double ceiling = camY + SPAWN_CEILING;
|
||||
double floor = Math.max(height, camY - SPAWN_FLOOR_MARGIN);
|
||||
if (ceiling <= floor) continue;
|
||||
x[i] = px;
|
||||
y[i] = floor + random.nextDouble() * (ceiling - floor);
|
||||
z[i] = pz;
|
||||
fall[i] = 0.6f + random.nextFloat() * 0.9f;
|
||||
size[i] = 0.55f + random.nextFloat() * 0.9f;
|
||||
alpha[i] = 0.45f + random.nextFloat() * 0.55f;
|
||||
phase[i] = random.nextFloat() * (float) (Math.PI * 2.0);
|
||||
rate[i] = 1.2f + random.nextFloat() * 2.2f;
|
||||
age[i] = 0f;
|
||||
vx[i] = (float) wind.x();
|
||||
vy[i] = -FALL_SPEED;
|
||||
vz[i] = (float) wind.z();
|
||||
ground[i] = height;
|
||||
columnX[i] = cx;
|
||||
columnZ[i] = cz;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.weather.blizzard.view;
|
||||
|
||||
/**
|
||||
* Белая пелена по образцу evo: штормовой туман сжимает дальность видимости и подмешивает цвет снега.
|
||||
* Сила пелены = сила шторма * экспозиция (под крышей пелены нет). Чистая математика.
|
||||
*/
|
||||
public final class BlizzardFog {
|
||||
|
||||
public static final float MIN_STRENGTH = 0.01f;
|
||||
public static final float START_SHARE = 0.05f;
|
||||
public static final float SKY_SHARE = 0.6f;
|
||||
public static final float BRIGHTNESS_GAIN = 1.6f;
|
||||
public static final float BRIGHTNESS_FLOOR = 0.15f;
|
||||
public static final float COLOR_BLEND = 0.9f;
|
||||
|
||||
private BlizzardFog() {
|
||||
}
|
||||
|
||||
public static boolean active(float strength) {
|
||||
return strength > MIN_STRENGTH;
|
||||
}
|
||||
|
||||
/** Дальность конца тумана: видимость в штиль, делённая на силу; слабая пелена почти не ограничивает обзор. */
|
||||
public static float endDistance(float visibility, float strength) {
|
||||
return visibility / strength;
|
||||
}
|
||||
|
||||
/** Дальность начала тумана. */
|
||||
public static float startDistance(float visibility, float strength) {
|
||||
return endDistance(visibility, strength) * START_SHARE;
|
||||
}
|
||||
|
||||
/** Туман только сжимает дальность, но не отодвигает ванильный. */
|
||||
public static float limit(float vanilla, float fog) {
|
||||
return Math.min(vanilla, fog);
|
||||
}
|
||||
|
||||
/** Яркость цвета снега: светлее небо, светлее пелена; всегда в 0..1. */
|
||||
public static float brightness(float r, float g, float b) {
|
||||
float v = Math.max(r, Math.max(g, b)) * BRIGHTNESS_GAIN + BRIGHTNESS_FLOOR;
|
||||
return Math.max(0.0f, Math.min(1.0f, v));
|
||||
}
|
||||
|
||||
/** Канал цвета тумана: от ванильного к оттенку снега по силе пелены. */
|
||||
public static float blend(float vanilla, float tintChannel, float brightness, float strength) {
|
||||
float t = strength * COLOR_BLEND;
|
||||
return vanilla + (tintChannel * brightness - vanilla) * t;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.weather.blizzard.view;
|
||||
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
|
||||
/**
|
||||
* Дымка и обмерзание краёв экрана по образцу evo: лёгкая заливка на весь экран и 32 рамки, каждая
|
||||
* следующая тоньше, поэтому непрозрачность копится к самому краю.
|
||||
*/
|
||||
public final class BlizzardOverlay {
|
||||
|
||||
public static final int FROST_BANDS = 32;
|
||||
public static final float FROST_DEPTH_SHARE = 0.2f;
|
||||
public static final float FROST_EDGE_ALPHA = 0.55f;
|
||||
public static final float HAZE_ALPHA = 0.12f;
|
||||
|
||||
private BlizzardOverlay() {
|
||||
}
|
||||
|
||||
/** @param current сила шторма * экспозиция * интенсивность */
|
||||
public static void render(GuiGraphicsExtractor ctx, float current, boolean frost, int tintArgb) {
|
||||
if (ctx == null || current <= 0.01f) return;
|
||||
int w = ctx.guiWidth();
|
||||
int h = ctx.guiHeight();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
ctx.fill(0, 0, w, h, withAlpha(tintArgb, hazeAlpha(current)));
|
||||
if (!frost) return;
|
||||
int color = withAlpha(tintArgb, frostBandAlpha(current));
|
||||
float depth = Math.min(w, h) * FROST_DEPTH_SHARE;
|
||||
for (int i = 0; i < FROST_BANDS; i++) {
|
||||
int t = Math.round(bandThickness(i, FROST_BANDS, depth));
|
||||
if (t <= 0) continue;
|
||||
ctx.fill(0, 0, w, t, color);
|
||||
ctx.fill(0, h - t, w, h, color);
|
||||
ctx.fill(0, t, t, h - t, color);
|
||||
ctx.fill(w - t, t, w, h - t, color);
|
||||
}
|
||||
}
|
||||
|
||||
/** Непрозрачность заливки на весь экран. */
|
||||
public static float hazeAlpha(float current) {
|
||||
return Math.max(0.0f, HAZE_ALPHA * current);
|
||||
}
|
||||
|
||||
/** Непрозрачность одной полосы обмерзания: все полосы вместе у края дают {@code 0.55 * current}. */
|
||||
public static float frostBandAlpha(float current) {
|
||||
return Math.max(0.0f, FROST_EDGE_ALPHA * current / FROST_BANDS);
|
||||
}
|
||||
|
||||
/** Толщина i-й полосы: первая на всю глубину, дальше тоньше, у последней почти ноль. */
|
||||
public static float bandThickness(int i, int bands, float depth) {
|
||||
return depth * (1.0f - (float) Math.sqrt(i / (double) bands));
|
||||
}
|
||||
|
||||
public static int withAlpha(int argb, float alpha) {
|
||||
int a = Math.max(0, Math.min(255, Math.round(alpha * 255.0f)));
|
||||
return (a << 24) | (argb & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
/** Доля полос обмерзания, покрывающая точку на distance от края экрана. */
|
||||
public static float edgeCoverage(float distance, float depth, int bands) {
|
||||
if (bands <= 0 || depth <= 0.0f) return 0.0f;
|
||||
if (distance <= 0.0f) return 1.0f;
|
||||
int covered = 0;
|
||||
for (int i = 0; i < bands; i++) {
|
||||
if (distance < bandThickness(i, bands, depth)) covered++;
|
||||
}
|
||||
return covered / (float) bands;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package dev.loki.lovisual.mixins.render.weather;
|
||||
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.blizzard.Blizzard;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.DeltaTracker;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.fog.FogData;
|
||||
import net.minecraft.client.renderer.fog.FogRenderer;
|
||||
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;
|
||||
|
||||
/** Штормовая пелена Blizzard: после ванильного тумана сжимает дальность и красит туман в цвет снега. */
|
||||
@Mixin(value = FogRenderer.class, priority = 1500)
|
||||
public class BlizzardFogMixin {
|
||||
|
||||
@Inject(method = "setupFog", at = @At("RETURN"))
|
||||
private void lovisual$blizzardWhiteout(Camera camera, int viewDistance, DeltaTracker tracker, float partial,
|
||||
ClientLevel level, CallbackInfoReturnable<FogData> cir) {
|
||||
Blizzard blizzard = Modules.get(Blizzard.class);
|
||||
if (blizzard != null) blizzard.applyFog(camera, cir.getReturnValue());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package dev.loki.lovisual.mixins.render.weather;
|
||||
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.blizzard.Blizzard;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.WeatherEffectRenderer;
|
||||
import net.minecraft.client.renderer.state.level.WeatherRenderState;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
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.CallbackInfo;
|
||||
|
||||
/** Blizzard убирает ванильные полосы дождя и снега, пока идёт шторм. */
|
||||
@Mixin(WeatherEffectRenderer.class)
|
||||
public class WeatherHideMixin {
|
||||
|
||||
@Inject(method = "extractRenderState(Lnet/minecraft/client/multiplayer/ClientLevel;FLnet/minecraft/world/phys/Vec3;Lnet/minecraft/client/renderer/state/level/WeatherRenderState;)V",
|
||||
at = @At("RETURN"))
|
||||
private void lovisual$dropVanillaColumns(ClientLevel level, float partial, Vec3 cameraPos,
|
||||
WeatherRenderState state, CallbackInfo ci) {
|
||||
Blizzard blizzard = Modules.get(Blizzard.class);
|
||||
if (blizzard == null || !blizzard.hidesVanillaWeather()) return;
|
||||
state.rainColumns.clear();
|
||||
state.snowColumns.clear();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue