feat(mod): GodRays, Knife melee skins, Wings Bedrock models, EnchantGlint animated styles, shader theme backdrop
- GodRays: PostProcessPass PRE_HAND, godrays.frag (radial sky blur to the sun over the scene, depth from mainDepth), GodRaysUniforms, GodRaysSkyMixin takes sunAngle/sunset colour from SkyRenderState; off under shaderpacks (Iris) - Knife (Melee Skins): 33 skins from Bedrock geo/anim in assets/lovisual/knife, animator (static_idle, draw/inspect/melee/run, 0.2 s crossfade, Catmull-Rom) with background baking, first/third-person render, sounds, mixins ItemInHand/PlayerItemInHand/Minecraft - Wings: wingSource Procedural/Model with 14 textured Bedrock models (WingModelDef/WingModels/WingModelRenderer, BedrockClip + procedural angel/demon/dragon); shared Bedrock parser util/bedrock (GeoParser, GeoFaces, Channel, Clip) - EnchantGlint: Pulse/Fire/Galaxy/Hologram/Glitch/Sparkle/Aurora styles in glint/style, coverage setting, custom animated texture matrix via GlintTextureMixin.styleMotion; lang en/ru for all new settings - Themes: shader backdrop for ClickGUI (Backdrop record, 6 modes, intensity/speed/scale, ui_theme_backdrop_batch.frag with 700 ms REVEAL/ERASE wipe, ThemeBackdrop2D in BackgroundComponent), ThemeEntry.backdrop persisted in JSON and profile codec, 6 presets with backdrops, backdrop settings in the theme editor - tests: GodRaysSun, GlintStyles, Knife assets/animator, WingModel, Backdrop, bedrock Channel; ./gradlew build green
This commit is contained in:
parent
6c641c4a94
commit
8bc884c297
278 changed files with 36225 additions and 56 deletions
31
mod/TODO.md
31
mod/TODO.md
|
|
@ -53,17 +53,30 @@
|
|||
константы ветра/плотности/турбулентности как у evo, экспозиция неба, «Whiteout» (миксин тумана `BlizzardFogMixin`), скрытие ванильной
|
||||
погоды (`WeatherHideMixin`), рендер «капля» вдоль скорости, обмерзание 32 полосы, «только в снегопад» по силе осадков.
|
||||
Не сделано из evo: «Snow cover» (снег накапливается на земле, шейдер). Не проверено вживую: вид, туман, положение концов крыльев.
|
||||
- [ ] Wings: режим «Модель» (Bedrock geo.json + animation.json, 14 видов; отчёт раздел 2.4). Шаги: `BedrockGeoParser` (общий с ножами),
|
||||
`BedrockAnimation` + процедурные angel/demon/dragon, `WingsRenderer.renderModel`, настройки `wingSource`/`wingModel`, тесты.
|
||||
Ресурсы (локально): `ref/evo/mod/jar/assets/evolution/cosmetic/wings/`, `.../textures/cosmetic/wings/`.
|
||||
- [ ] Knife (Melee Skins): 33 скина, клипы draw/inspect/melee_1..3/run, first/third person, звуки, шейдер holy (отчёт раздел 3.7,
|
||||
порядок работ из 8 шагов). Ресурсы: `ref/evo/mod/jar/assets/evolution/cosmetic/melee/`.
|
||||
- [x] Темы с шейдерным фоном как у evo (2026-10-05, вживую не смотрели; фон только за ClickGUI, переход темы сразу). Сделано: `theme/impl/Backdrop`, поле `ThemeEntry.backdrop`, JSON и профиль, шейдер `ui_theme_backdrop_batch.frag` (6 режимов + волна REVEAL/ERASE 700 мс), `ThemeBackdrop2D`, рисование в `BackgroundComponent`, 6 пресетов с фоном, редактор тем (режим/сила/скорость/масштаб). Не сделано: превью фона в карточках тем, lang-ключи редактора (их у редактора нет и для остальных полей). План был:
|
||||
1. `Themes.Backdrop(mode, intensity, speed, scale)` в `ThemeEntry`; `ThemeStore`/`ThemesProfileCodec` пишут и читают его, старые темы = None.
|
||||
2. Шейдер `theme_backdrop.frag` (Iridescence, Magma, Digital Rain, Grid Horizon, Singularity, Ember), цвета из accent/accentSoft/windowBg.
|
||||
3. Рисование за окном ClickGUI через пайплайн полноэкранных проходов (`ScreenPipelines.screen`), uniform `ThemeBackdrop`.
|
||||
4. Шейдерный переход при смене темы (`theme_wipe`): волна от нового к старому за время `THEME_TRANSITION_MS`.
|
||||
5. Пресеты с включённым фоном (Cinder Magma, Dusk Grid Horizon, Prism Iridescence, Forest Digital Rain, Nitro Singularity, Amber Ember).
|
||||
6. Редактор тем: режим, интенсивность, скорость, масштаб; lang en/ru; тесты на чистую логику (Backdrop.lerp, кодек, режимы).
|
||||
- [x] Wings: режим «Модель» (2026-10-04, вживую не смотрели): `Wings` получил `wingSource` (Procedural/Model) и `wingModel` (14 видов).
|
||||
Общий парсер Bedrock в `util/bedrock/{geo,anim}` (`GeoParser`, `GeoFaces`, `Channel`, `Clip`), крылья в `visuals/wings/model/`
|
||||
(`WingModelDef` каталог, `WingModels` загрузка, `WingModelRenderer` якорь как у игрока-модели + присед, `anim/ProceduralWings`
|
||||
angel/demon/dragon, `anim/BedrockClip`). Ресурсы скопированы из evo в `assets/lovisual/{wings,textures/wings}` (лицензию решать до релиза).
|
||||
Проверить в игре: сторона/направление X, Z-сдвиг от спины, зеркальность текстур на гранях, свет (множитель яркости, не lightmap).
|
||||
- [x] Knife (Melee Skins) (2026-10-04, вживую не смотрели): `visuals/knife/` (модуль `Knife`, каталог `KnifeSkin` из `knife/index.json`,
|
||||
`anim/` аниматор как `Inspect` у evo: static_idle + draw/inspect/melee_1..3 + аддитивный бег, кроссфейд 0.2 с, Catmull-Rom;
|
||||
`model/` бейк геометрии и клипов в фоновом потоке; `render/` первое лицо с руками игрока в `righthand_pos/lefthand_pos` и камерой,
|
||||
третье лицо у себя). Миксины `mixins/render/knife/*` (`ItemInHandRenderer.submitArmWithItem`, `PlayerItemInHandLayer`, `Minecraft`
|
||||
ввод). Ресурсы из evo в `assets/lovisual/{knife,textures/knife,sounds/knife,sounds.json}` (звуки cs2 и скины CS: лицензию решать до релиза).
|
||||
Не сделано: шейдер holy (кости `holyBones` рисуются обычной текстурой), нож в левой (оффхенд) руке, иконки слотов, выбор скина в UI вне ModeValue.
|
||||
- [x] Быстрые модули P1 (2026-10-02, сверены с `ref/evo/mod/jar-src-named/evovis/module/{misc,render}`, тесты зелёные, вживую не смотрели):
|
||||
AutoSprint (`player/`, как у evo держит спринт; вход через `SprintControlEvent`, решает `SprintController.canStartSprinting`),
|
||||
TotemCounter (`visuals/totems/`, свои тотемы справа от хотбара; счёт попов чужих у нас уже в `util/pvp/opponents/TotemPopSnapshot`,
|
||||
без отдельной клавиши сброса как у evo), EnchantGlint (`visuals/items/glint/`: стили Rainbow/Solid/Gradient/Hidden, сила, скорость,
|
||||
без отдельной клавиши сброса как у evo), EnchantGlint (`visuals/items/glint/`: стили Rainbow/Solid/Gradient/Hidden (2026-10-04 добавлены Pulse/Fire/Galaxy/Hologram/Glitch/Sparkle/Aurora, Coverage и своя анимация текстурной матрицы: `glint/style/`, `GlintTextureMixin.styleMotion`), сила, скорость,
|
||||
масштаб, «предметы/броня»; у evo свои RenderType и 11 стилей, у нас перерисовка ванильных текстур блеска `GlintTexture` +
|
||||
`GlintTextureMixin`; нет Pulse/Fire/Galaxy/Hologram/Glitch/Sparkle/Aurora, Coverage), ArmorColor (`visuals/items/armor/`: цвет по
|
||||
`GlintTextureMixin`; не сделано: режим «Special» трезубец/щит), ArmorColor (`visuals/items/armor/`: цвет по
|
||||
прочности Gradient/Steps, цели Players/Friends/Self/Mobs, Only Below, Low Pulse; миксин в `EquipmentLayerRendererMixin`, флаг цели
|
||||
в `LivingEntityRenderState`), PlaceAnimation (`visuals/placeanimation/`: блок вылетает из руки, наклон/масштаб, как у evo: скрытие из
|
||||
меша `SectionCompilerPlaceMixin` + Sodium `SodiumPlace*Mixin`, отрисовка движущимся блоком `LevelRendererPlaceMixin`, блок-сущности
|
||||
|
|
@ -78,7 +91,9 @@
|
|||
- [x] CustomModels (2026-10-02, из `CustomModels-2026-10-02-120830.zip`): `visuals/models/` + `mesh/` (Chicken, Rabbit, Verity из .bbmodel),
|
||||
ассеты в `assets/lovisual/models/`, миксины `render/models/`. Только у вас на экране, цели: себя/друзья/все. Нужна проверка вживую
|
||||
(поза рук, рука от первого лица, Verity под Iris/Sodium). Исходный проект зарезан до этого модуля, остальные его части не нужны.
|
||||
- [ ] GodRays (референс `shaders/core/godrays`).
|
||||
- [x] GodRays (2026-10-04, вживую не смотрели): `visuals/godrays/` (модуль `GodRays` как `PostProcessPass` PRE_HAND, чистая математика `GodRaysSun`),
|
||||
шейдер `godrays.frag` (один проход: радиальный блюр неба к солнцу поверх сцены, глубина из `context.mainDepth()`), `GodRaysUniforms`,
|
||||
миксин `render/godrays/GodRaysSkyMixin` забирает `sunAngle` и закатный цвет из `SkyRenderState`. Не работает при шейдерпаке (Iris).
|
||||
- [ ] Дальше P1-P2 — перенос модулей из evo поштучно (референс: `docs/EVO_REPORT.md` §1.2;
|
||||
лицензия evo проприетарная — их ассеты/код в репо не кладём, только идеи):
|
||||
- [ ] **Blizzard** (render, evo ~1290 LOC, M/L) — высотная метель: ветровые снежинки с горизонтальным
|
||||
|
|
|
|||
|
|
@ -1,11 +1,31 @@
|
|||
package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.other;
|
||||
|
||||
import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer;
|
||||
import dev.loki.lovisual.features.theme.Themes;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.renderer2d.fx.backdrop.ThemeBackdrop2D;
|
||||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette;
|
||||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D;
|
||||
|
||||
public final class BackgroundComponent {
|
||||
|
||||
/** The theme's shader backdrop, or both sides of the wipe while the theme is changing. */
|
||||
private static void renderBackdrop(float x, float y, float w, float h, float radius) {
|
||||
Themes.BackdropView view = Themes.backdropView();
|
||||
Themes.Theme theme = dev.loki.lovisual.features.theme.Theme.theme();
|
||||
float opacity = ClickGuiRenderer.getRenderAlphaMultiplier();
|
||||
Renderer2D r = Renderer2D.COLOR;
|
||||
if (view.wipe() < 1.0f) {
|
||||
ThemeBackdrop2D.draw(r, x, y, w, h, radius, view.outgoing(), theme.accent(), theme.textPrimary(), opacity,
|
||||
ThemeBackdrop2D.Wipe.ERASE, view.wipe());
|
||||
ThemeBackdrop2D.draw(r, x, y, w, h, radius, view.target(), theme.accent(), theme.textPrimary(), opacity,
|
||||
ThemeBackdrop2D.Wipe.REVEAL, view.wipe());
|
||||
} else {
|
||||
ThemeBackdrop2D.draw(r, x, y, w, h, radius, view.target(), theme.accent(), theme.textPrimary(), opacity,
|
||||
ThemeBackdrop2D.Wipe.NONE, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public void render(float x, float y, float w, float h, float scale) {
|
||||
SettingsGuiPalette palette = SettingsGuiPalette.current();
|
||||
ClickGuiRenderer.drawBlur(x, y, w, h, 8f * scale, 0xFF000000, 200f / 255f);
|
||||
|
|
@ -16,6 +36,7 @@ public final class BackgroundComponent {
|
|||
palette.menuWindowBgRight(),
|
||||
palette.menuWindowBgLeft()
|
||||
);
|
||||
renderBackdrop(x, y, w, h, 8f * scale);
|
||||
LayoutRender2D.roundedStroke(x, y, w, h, 8f * scale, 0.1f * scale, palette.menuWindowStroke());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.godrays;
|
||||
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import dev.loki.lovisual.config.values.color.RGBColorValue;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
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.render.engine.RenderState;
|
||||
import dev.loki.lovisual.render.engine.core.system.LoVisualWorldMatrices;
|
||||
import dev.loki.lovisual.render.engine.optimize.OptimizeState;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessContext;
|
||||
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager;
|
||||
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass;
|
||||
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.godrays.GodRaysUniforms;
|
||||
import dev.loki.lovisual.render.iris.runtime.IrisRuntime;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Quaternionf;
|
||||
|
||||
/**
|
||||
* Sun shafts: a radial blur of the bright sky towards the sun, added on top of the scene. Skipped while a
|
||||
* shaderpack renders; the sun position comes from the sky state captured by GodRaysSkyMixin.
|
||||
*/
|
||||
@ModuleInfo(
|
||||
id = "godrays",
|
||||
displayName = "God Rays",
|
||||
aliases = {"godrays", "sunshafts"},
|
||||
category = ModuleCategory.VISUALS
|
||||
)
|
||||
public final class GodRays extends Module implements PostProcessPass {
|
||||
private static final float SAMPLE_WEIGHT = 1.35f;
|
||||
private static final float MIN_FADE = 0.002f;
|
||||
private static final int LITE_SAMPLES = 12;
|
||||
|
||||
private final NumberValue<Float> strength = num("godRaysStrength", "strength", 110.0f, 10.0f, 400.0f);
|
||||
private final NumberValue<Float> reach = num("godRaysReach", "reach", 65.0f, 20.0f, 100.0f);
|
||||
private final NumberValue<Integer> samples = num("godRaysSamples", "samples", 28, 8, 64);
|
||||
private final NumberValue<Float> falloff = num("godRaysFalloff", "falloff", 96.0f, 80.0f, 100.0f);
|
||||
private final RGBColorValue rayColor = colorNoAlpha("godRaysColor", "color", "#FFE2A8");
|
||||
private final BooleanValue sunsetBoost = bool("godRaysSunsetBoost", "sunset_boost", true);
|
||||
|
||||
private final Matrix4f viewProjection = new Matrix4f();
|
||||
private boolean failed;
|
||||
|
||||
{
|
||||
PostProcessManager.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
failed = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
return isEnabled() && !failed && GodRaysSun.isValid() && mc.player != null && mc.level != null
|
||||
&& !IrisRuntime.isShaderpackRendererActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Phase getPhase() {
|
||||
return Phase.PRE_HAND;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean render(PostProcessContext context, GpuTextureView src, GpuTextureView dst) {
|
||||
if (!isActive() || context == null || src == null || dst == null || context.mainDepth() == null) return false;
|
||||
|
||||
Matrix4f projection = LoVisualWorldMatrices.renderProjectionMatrix();
|
||||
if (projection == null) projection = RenderState.worldProjection;
|
||||
viewProjection.set(projection).mul(new Matrix4f().rotation(new Quaternionf(RenderState.cameraRotation).conjugate()));
|
||||
|
||||
GodRaysSun.Screen sun = GodRaysSun.project(GodRaysSun.angle(), viewProjection);
|
||||
if (sun == null) return false;
|
||||
float fade = GodRaysSun.fade(sun, Minecraft.getInstance().level.getRainLevel(1.0f), sunsetBoost.get());
|
||||
if (fade <= MIN_FADE) return false;
|
||||
|
||||
int glow = GodRaysSun.sunrise();
|
||||
int ray = rayColor.getArgb();
|
||||
int glowAlpha = glow >>> 24;
|
||||
GodRaysUniforms.update(sun.u(), sun.v(), reach.get() * 0.01f,
|
||||
OptimizeState.isLitePost() ? LITE_SAMPLES : samples.get(), SAMPLE_WEIGHT,
|
||||
strength.get() * 0.01f, falloff.get() * 0.01f,
|
||||
GodRaysSun.tintChannel(ray >> 16 & 0xFF, glow >> 16 & 0xFF, glowAlpha),
|
||||
GodRaysSun.tintChannel(ray >> 8 & 0xFF, glow >> 8 & 0xFF, glowAlpha),
|
||||
GodRaysSun.tintChannel(ray & 0xFF, glow & 0xFF, glowAlpha),
|
||||
fade, context.width(), context.height());
|
||||
try {
|
||||
FullScreenRenderer.ensureInit();
|
||||
FullScreenRenderer.begin("LoVisual GodRays Pass")
|
||||
.attachment(dst)
|
||||
.pipeline(LoVisualRenderPipelines.GOD_RAYS)
|
||||
.uniform("GodRays", GodRaysUniforms.get())
|
||||
.sampler("u_Texture", src, PostProcessManager.getSampler())
|
||||
.sampler("u_MainDepth", context.mainDepth(), PostProcessManager.getSampler())
|
||||
.end();
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
failed = true;
|
||||
DebugLog.error("[LoVisual] GodRays pass failed, disabled until re-enabled", t);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.godrays;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/** Sky state captured each frame plus the pure maths that turns it into a screen position and a fade. */
|
||||
public final class GodRaysSun {
|
||||
/** Sun position on screen in 0..1 uv together with how high it stands (-1..1). */
|
||||
public record Screen(float u, float v, float elevation) {
|
||||
}
|
||||
|
||||
private static final float HORIZON_FADE_START = 0.03f;
|
||||
private static final float HORIZON_FADE_END = 0.2f;
|
||||
private static final float OFF_SCREEN_RANGE = 0.55f;
|
||||
private static final float RAIN_FADE = 0.85f;
|
||||
private static final float SUNSET_GAIN = 0.8f;
|
||||
private static final float SUNSET_REFERENCE = 0.45f;
|
||||
private static final float WARMTH = 0.85f;
|
||||
private static final float W_EPSILON = 1.0E-6f;
|
||||
|
||||
private static volatile float sunAngle;
|
||||
private static volatile int sunriseColor;
|
||||
private static volatile boolean valid;
|
||||
|
||||
private GodRaysSun() {
|
||||
}
|
||||
|
||||
public static void capture(float angle, int sunriseAndSunsetColor) {
|
||||
sunAngle = angle;
|
||||
sunriseColor = sunriseAndSunsetColor;
|
||||
valid = true;
|
||||
}
|
||||
|
||||
public static void invalidate() {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
public static boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public static float angle() {
|
||||
return sunAngle;
|
||||
}
|
||||
|
||||
public static int sunrise() {
|
||||
return sunriseColor;
|
||||
}
|
||||
|
||||
/** Projects the sun direction; null while it is below the horizon or behind the camera. */
|
||||
public static Screen project(float angle, Matrix4f viewProjection) {
|
||||
float elevation = (float) Math.cos(angle);
|
||||
if (elevation <= HORIZON_FADE_START) return null;
|
||||
Vector4f clip = new Vector4f(-(float) Math.sin(angle), elevation, 0.0f, 0.0f).mul(viewProjection);
|
||||
if (clip.w <= W_EPSILON) return null;
|
||||
return new Screen(clip.x / clip.w * 0.5f + 0.5f, clip.y / clip.w * 0.5f + 0.5f, elevation);
|
||||
}
|
||||
|
||||
public static float fade(Screen sun, float rain, boolean sunsetBoost) {
|
||||
float horizon = smoothstep(HORIZON_FADE_START, HORIZON_FADE_END, sun.elevation());
|
||||
float overshoot = Math.max(Math.max(Math.abs(sun.u() - 0.5f) - 0.5f, 0.0f), Math.max(Math.abs(sun.v() - 0.5f) - 0.5f, 0.0f));
|
||||
float screen = 1.0f - smoothstep(0.0f, OFF_SCREEN_RANGE, overshoot);
|
||||
float weather = 1.0f - Math.max(0.0f, Math.min(rain, 1.0f)) * RAIN_FADE;
|
||||
float boost = sunsetBoost ? 1.0f + SUNSET_GAIN * (1.0f - Math.min(sun.elevation() / SUNSET_REFERENCE, 1.0f)) : 1.0f;
|
||||
return horizon * screen * weather * boost;
|
||||
}
|
||||
|
||||
/** One tint channel: the ray colour pulled towards the sunrise/sunset glow while there is one. */
|
||||
public static float tintChannel(int rayChannel, int glowChannel, int glowAlpha) {
|
||||
float warmth = glowAlpha / 255.0f * WARMTH;
|
||||
float glow = glowChannel / 255.0f;
|
||||
return rayChannel / 255.0f * (1.0f - warmth + glow * warmth);
|
||||
}
|
||||
|
||||
static float smoothstep(float from, float to, float x) {
|
||||
float t = Math.max(0.0f, Math.min((x - from) / (to - from), 1.0f));
|
||||
return t * t * (3.0f - 2.0f * t);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,10 +7,15 @@ import dev.loki.lovisual.config.values.primitive.NumberValue;
|
|||
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.modules.visuals.items.glint.style.GlintMotion;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.glint.style.GlintStyles;
|
||||
import net.minecraft.util.Util;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/**
|
||||
* Enchantment glint on items and armor: style (Rainbow, Solid, Gradient, Hidden), colours, strength, scroll
|
||||
* speed and stripe scale. Speed and scale go through GlintTextureMixin, the look by repainting the textures.
|
||||
* Enchantment glint on items and armor: eleven styles (Rainbow, Solid, Gradient, Pulse, Fire, Galaxy, Hologram,
|
||||
* Glitch, Sparkle, Aurora, Hidden), colours, strength, coverage, scroll speed and stripe scale. The look is a
|
||||
* repaint of the textures; speed, scale and the animated styles' motion go through GlintTextureMixin.
|
||||
*/
|
||||
@ModuleInfo(
|
||||
id = "enchantglint",
|
||||
|
|
@ -20,12 +25,15 @@ import dev.loki.lovisual.features.module.core.ModuleInfo;
|
|||
)
|
||||
public class EnchantGlint extends Module {
|
||||
private final ModeValue style = modeSetting("enchantGlintStyle", "style", GlintTint.RAINBOW,
|
||||
GlintTint.RAINBOW, GlintTint.SOLID, GlintTint.GRADIENT, GlintTint.HIDDEN);
|
||||
GlintTint.RAINBOW, GlintTint.SOLID, GlintTint.GRADIENT, GlintStyles.PULSE, GlintStyles.FIRE,
|
||||
GlintStyles.GALAXY, GlintStyles.HOLOGRAM, GlintStyles.GLITCH, GlintStyles.SPARKLE, GlintStyles.AURORA,
|
||||
GlintTint.HIDDEN);
|
||||
private final RGBColorValue color =
|
||||
visibleWhen(colorNoAlpha("enchantGlintColor", "color", "#FF4FD8"), this::usesPrimary);
|
||||
private final RGBColorValue secondColor =
|
||||
visibleWhen(colorNoAlpha("enchantGlintColor2", "color2", "#4F9BFF"), this::usesSecondary);
|
||||
private final NumberValue<Float> strength = num("enchantGlintStrength", "strength", 1.5f, 0.2f, 3.0f);
|
||||
private final NumberValue<Float> coverage = num("enchantGlintCoverage", "coverage", 0.35f, 0.0f, 1.0f);
|
||||
private final NumberValue<Float> speed = num("enchantGlintSpeed", "speed", 1.0f, 0.1f, 4.0f);
|
||||
private final NumberValue<Float> scale = num("enchantGlintScale", "scale", 1.0f, 0.2f, 4.0f);
|
||||
private final BooleanValue items = bool("enchantGlintItems", "items", true);
|
||||
|
|
@ -34,17 +42,18 @@ public class EnchantGlint extends Module {
|
|||
private final GlintTexture texture = new GlintTexture();
|
||||
|
||||
private boolean usesPrimary() {
|
||||
return GlintTint.SOLID.equals(style.get()) || GlintTint.GRADIENT.equals(style.get());
|
||||
String current = style.get();
|
||||
return !GlintTint.RAINBOW.equals(current) && !GlintTint.HIDDEN.equals(current);
|
||||
}
|
||||
|
||||
private boolean usesSecondary() {
|
||||
return GlintTint.GRADIENT.equals(style.get());
|
||||
return GlintTint.GRADIENT.equals(style.get()) || GlintStyles.usesSecondary(style.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTick() {
|
||||
GlintTexture.Paint paint = new GlintTexture.Paint(style.get(),
|
||||
color.getArgb() & 0xFFFFFF, secondColor.getArgb() & 0xFFFFFF, strength.get());
|
||||
color.getArgb() & 0xFFFFFF, secondColor.getArgb() & 0xFFFFFF, strength.get(), coverage.get());
|
||||
texture.sync(items.get() ? paint : null, armor.get() ? paint : null);
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +67,12 @@ public class EnchantGlint extends Module {
|
|||
return isEnabled() ? speed.get() : 1.0f;
|
||||
}
|
||||
|
||||
/** Animated texture matrix of the current style, or null while the style keeps the vanilla motion. */
|
||||
public Matrix4f motion(float scale) {
|
||||
if (!isEnabled()) return null;
|
||||
return GlintMotion.matrix(style.get(), Util.getMillis() * 0.001f * speed.get(), scale);
|
||||
}
|
||||
|
||||
public float scaleFactor() {
|
||||
return isEnabled() ? scale.get() : 1.0f;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import java.util.Optional;
|
|||
*/
|
||||
final class GlintTexture {
|
||||
/** What to paint onto a glint texture; a null paint means "leave it vanilla". */
|
||||
record Paint(String style, int primary, int secondary, float strength) {
|
||||
record Paint(String style, int primary, int secondary, float strength, float coverage) {
|
||||
}
|
||||
|
||||
private final Map<AbstractTexture, Paint> painted = new IdentityHashMap<>();
|
||||
|
|
@ -65,7 +65,7 @@ final class GlintTexture {
|
|||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
image.setPixel(x, y, GlintTint.pixel(image.getPixel(x, y), (x + 0.5f) / width, (y + 0.5f) / height,
|
||||
paint.style(), paint.primary(), paint.secondary(), paint.strength()));
|
||||
paint.style(), paint.primary(), paint.secondary(), paint.strength(), paint.coverage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.glint;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.glint.style.GlintStyles;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
/** Pure pixel maths for repainting the enchantment glint texture (ARGB ints, u and v in 0..1). */
|
||||
|
|
@ -15,19 +16,34 @@ public final class GlintTint {
|
|||
private GlintTint() {
|
||||
}
|
||||
|
||||
/** Colours the glint streaks in a style; the pixel's strongest channel keeps the streak shape. */
|
||||
public static int pixel(int argb, float u, float v, String style, int primary, int secondary, float strength) {
|
||||
return pixel(argb, u, v, style, primary, secondary, strength, 0.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Colours the glint streaks in a style; the pixel's strongest channel keeps the streak shape and
|
||||
* {@code coverage} lifts the glow between the streaks.
|
||||
*/
|
||||
public static int pixel(int argb, float u, float v, String style, int primary, int secondary, float strength, float coverage) {
|
||||
if (HIDDEN.equals(style)) return argb & 0x00FFFFFF;
|
||||
float level = Math.min(1.0f, (Math.max(argb >> 16 & 0xFF, Math.max(argb >> 8 & 0xFF, argb & 0xFF)) / 255.0f) * strength);
|
||||
float streak = Math.max(argb >> 16 & 0xFF, Math.max(argb >> 8 & 0xFF, argb & 0xFF)) / 255.0f;
|
||||
if (GlintStyles.handles(style)) {
|
||||
return (argb & 0xFF000000) | scaled(GlintStyles.shade(style, u, v, streak, primary, secondary, coverage), strength);
|
||||
}
|
||||
float level = Math.min(1.0f, (coverage + (1.0f - coverage) * streak) * strength);
|
||||
int color = switch (style) {
|
||||
case RAINBOW -> Mth.hsvToRgb(frac(u * (1.0f - RAINBOW_SKEW) + v * RAINBOW_SKEW), 1.0f, 1.0f);
|
||||
case GRADIENT -> lerp(primary, secondary, frac(u * (1.0f - GRADIENT_SKEW) + v * GRADIENT_SKEW));
|
||||
default -> primary;
|
||||
};
|
||||
int r = Math.round((color >> 16 & 0xFF) * level);
|
||||
int g = Math.round((color >> 8 & 0xFF) * level);
|
||||
int b = Math.round((color & 0xFF) * level);
|
||||
return (argb & 0xFF000000) | r << 16 | g << 8 | b;
|
||||
return (argb & 0xFF000000) | scaled(color, level);
|
||||
}
|
||||
|
||||
private static int scaled(int rgb, float factor) {
|
||||
int r = Math.min(255, Math.round((rgb >> 16 & 0xFF) * factor));
|
||||
int g = Math.min(255, Math.round((rgb >> 8 & 0xFF) * factor));
|
||||
int b = Math.min(255, Math.round((rgb & 0xFF) * factor));
|
||||
return r << 16 | g << 8 | b;
|
||||
}
|
||||
|
||||
static int lerp(int from, int to, float t) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.glint.style;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/** Texture-matrix animation of the procedural glint styles; null for styles that keep vanilla motion. */
|
||||
public final class GlintMotion {
|
||||
private static final float DEFAULT_ROTATION = 0.17453292f;
|
||||
|
||||
private GlintMotion() {
|
||||
}
|
||||
|
||||
/** {@code seconds} is the speed-scaled clock; {@code scale} the vanilla texture scale. */
|
||||
public static Matrix4f matrix(String style, float seconds, float scale) {
|
||||
return switch (style) {
|
||||
case GlintStyles.PULSE -> build(seconds * 0.073f, seconds * 0.267f, DEFAULT_ROTATION,
|
||||
scale * (1.0f + 0.22f * (float) Math.sin(seconds * 1.6f)));
|
||||
case GlintStyles.FIRE -> build(0.05f * (float) Math.sin(seconds * 0.7f), -seconds * 0.45f, 0.0f, scale);
|
||||
case GlintStyles.GALAXY -> build(seconds * 0.02f, seconds * 0.015f, DEFAULT_ROTATION + seconds * 0.035f, scale);
|
||||
case GlintStyles.HOLOGRAM -> build(seconds * 0.04f, seconds * 0.7f, 0.0f, scale);
|
||||
case GlintStyles.GLITCH -> {
|
||||
int step = (int) Math.floor(seconds * 7.0f);
|
||||
yield build(GlintNoise.hash(step, 0, 64, 31), GlintNoise.hash(0, step, 64, 47), 0.0f, scale);
|
||||
}
|
||||
case GlintStyles.SPARKLE -> build(seconds * 0.11f, seconds * 0.07f, DEFAULT_ROTATION + seconds * 0.14f, scale);
|
||||
case GlintStyles.AURORA -> build(seconds * 0.05f, seconds * 0.12f, 0.08726646f, scale);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private static Matrix4f build(float du, float dv, float rotation, float scale) {
|
||||
return new Matrix4f().translation(-wrap(du), wrap(dv), 0.0f).rotateZ(rotation).scale(scale);
|
||||
}
|
||||
|
||||
static float wrap(float x) {
|
||||
return x - (float) Math.floor(x);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.glint.style;
|
||||
|
||||
/** Tileable procedural noise for the glint styles; every cell lookup wraps with the grid period. */
|
||||
public final class GlintNoise {
|
||||
private GlintNoise() {
|
||||
}
|
||||
|
||||
/** Integer hash of a wrapped grid cell, 0..1. */
|
||||
public static float hash(int x, int y, int period, int seed) {
|
||||
int h = Math.floorMod(x, period) * 0x1F3D5B79 + Math.floorMod(y, period) * 0x2545F491 + seed * 0x6C8E9CF5;
|
||||
h ^= h >>> 16;
|
||||
h *= 0x7FEB352D;
|
||||
h ^= h >>> 15;
|
||||
h *= 0x846CA68B;
|
||||
h ^= h >>> 16;
|
||||
return (h >>> 8) * (1.0f / (1 << 24));
|
||||
}
|
||||
|
||||
public static float smooth(float t) {
|
||||
return t * t * (3.0f - 2.0f * t);
|
||||
}
|
||||
|
||||
public static float wave(float radians) {
|
||||
return 0.5f + 0.5f * (float) Math.cos(radians);
|
||||
}
|
||||
|
||||
/** Bilinear value noise on a {@code cells} x {@code cells} wrapping grid. */
|
||||
public static float value(float u, float v, int cells, int seed) {
|
||||
float x = u * cells;
|
||||
float y = v * cells;
|
||||
int ix = (int) Math.floor(x);
|
||||
int iy = (int) Math.floor(y);
|
||||
float fx = smooth(x - ix);
|
||||
float fy = smooth(y - iy);
|
||||
float top = lerp(fx, hash(ix, iy, cells, seed), hash(ix + 1, iy, cells, seed));
|
||||
float bottom = lerp(fx, hash(ix, iy + 1, cells, seed), hash(ix + 1, iy + 1, cells, seed));
|
||||
return lerp(fy, top, bottom);
|
||||
}
|
||||
|
||||
/** Fractal sum of value noise, each octave doubling the grid and halving the weight; result 0..1. */
|
||||
public static float fbm(float u, float v, int octaves, int baseCells, int seed) {
|
||||
float sum = 0.0f;
|
||||
float weight = 1.0f;
|
||||
float total = 0.0f;
|
||||
int cells = baseCells;
|
||||
for (int i = 0; i < octaves; i++) {
|
||||
sum += value(u, v, cells, seed + i) * weight;
|
||||
total += weight;
|
||||
weight *= 0.5f;
|
||||
cells <<= 1;
|
||||
}
|
||||
return sum / total;
|
||||
}
|
||||
|
||||
/** Nearest-point glitter: one jittered point per cell with a soft round falloff; result 0..1. */
|
||||
public static float sparkles(float u, float v, int cells, float radius, float density, int seed) {
|
||||
float x = u * cells;
|
||||
float y = v * cells;
|
||||
int cx = (int) Math.floor(x);
|
||||
int cy = (int) Math.floor(y);
|
||||
float best = 0.0f;
|
||||
for (int dy = -1; dy <= 1; dy++) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
int nx = cx + dx;
|
||||
int ny = cy + dy;
|
||||
if (hash(nx, ny, cells, seed) > density) continue;
|
||||
float px = nx + 0.15f + hash(nx, ny, cells, seed + 101) * 0.7f;
|
||||
float py = ny + 0.15f + hash(nx, ny, cells, seed + 202) * 0.7f;
|
||||
float distance = (float) Math.hypot((x - px) / radius, (y - py) / radius);
|
||||
float glow = 1.0f - distance;
|
||||
if (glow > 0.0f) best = Math.max(best, glow * glow);
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
public static float lerp(float t, float a, float b) {
|
||||
return a + t * (b - a);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.glint.style;
|
||||
|
||||
/**
|
||||
* Colour of one glint texel for the procedural styles. {@code streak} is the vanilla texture's own brightness
|
||||
* pattern (0..1), so each style keeps the familiar diagonal streaks and adds its own look on top.
|
||||
*/
|
||||
public final class GlintStyles {
|
||||
public static final String PULSE = "Pulse";
|
||||
public static final String FIRE = "Fire";
|
||||
public static final String GALAXY = "Galaxy";
|
||||
public static final String HOLOGRAM = "Hologram";
|
||||
public static final String GLITCH = "Glitch";
|
||||
public static final String SPARKLE = "Sparkle";
|
||||
public static final String AURORA = "Aurora";
|
||||
|
||||
private static final float TAU = (float) (Math.PI * 2.0);
|
||||
|
||||
private GlintStyles() {
|
||||
}
|
||||
|
||||
public static boolean handles(String style) {
|
||||
return switch (style) {
|
||||
case PULSE, FIRE, GALAXY, HOLOGRAM, GLITCH, SPARKLE, AURORA -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/** True when the style blends two colours rather than using the primary alone. */
|
||||
public static boolean usesSecondary(String style) {
|
||||
return switch (style) {
|
||||
case FIRE, GALAXY, HOLOGRAM, GLITCH, AURORA -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/** RGB (0xRRGGBB) before strength is applied; coverage lifts the glow between streaks. */
|
||||
public static int shade(String style, float u, float v, float streak, int primary, int secondary, float coverage) {
|
||||
float[] color = new float[3];
|
||||
float level;
|
||||
switch (style) {
|
||||
case PULSE -> {
|
||||
set(color, primary);
|
||||
level = streak * (0.15f + 0.85f * (float) Math.pow(GlintNoise.wave(TAU * u), 2.0));
|
||||
}
|
||||
case FIRE -> {
|
||||
float heat = clamp((GlintNoise.fbm(u, v, 5, 3, 7) - 0.3f) * 2.4f);
|
||||
mix(color, primary, secondary, GlintNoise.smooth(heat));
|
||||
lift(color, 0.45f + 0.55f * heat, (float) Math.pow(heat, 6.0) * 0.65f);
|
||||
level = 0.35f + 0.65f * streak;
|
||||
}
|
||||
case GALAXY -> {
|
||||
float cloud = clamp((GlintNoise.fbm(u, v, 5, 3, 21) - 0.28f) * 2.4f);
|
||||
mix(color, primary, secondary, (float) Math.pow(cloud, 3.0));
|
||||
lift(color, 0.3f + 0.7f * cloud, GlintNoise.sparkles(u, v, 12, 0.22f, 0.35f, 33));
|
||||
level = 0.4f + 0.6f * streak;
|
||||
}
|
||||
case HOLOGRAM -> {
|
||||
float scan = (float) Math.pow(GlintNoise.wave(16.0f * TAU * v), 2.0);
|
||||
mix(color, primary, secondary, GlintNoise.wave(TAU * (v * 0.6f + u * 0.15f)));
|
||||
level = clamp(streak * (0.35f + 0.8f * scan));
|
||||
}
|
||||
case GLITCH -> {
|
||||
int cx = (int) Math.floor(u * 16.0f);
|
||||
int cy = (int) Math.floor(v * 16.0f);
|
||||
if (GlintNoise.hash(cx, cy, 16, 11) <= 0.45f) return 0;
|
||||
float pick = GlintNoise.hash(cx, cy, 16, 977);
|
||||
if (pick > 0.25f) mix(color, primary, secondary, pick);
|
||||
else set(color, 0xFFFFFF);
|
||||
level = 0.4f + 0.6f * GlintNoise.hash(cx, cy, 16, 613);
|
||||
}
|
||||
case SPARKLE -> {
|
||||
float glitter = GlintNoise.sparkles(u, v, 14, 0.3f, 0.55f, 5);
|
||||
set(color, primary);
|
||||
for (int i = 0; i < 3; i++) color[i] = 1.0f + 0.55f * (color[i] - 1.0f);
|
||||
level = clamp(glitter + streak * 0.25f);
|
||||
}
|
||||
case AURORA -> {
|
||||
float ribbon = GlintNoise.wave(TAU * (u * 2.0f + 0.22f * (float) Math.cos(TAU * v)));
|
||||
mix(color, primary, secondary, ribbon);
|
||||
level = clamp((float) Math.pow(ribbon, 4.0) * (0.5f + 0.7f * streak));
|
||||
}
|
||||
default -> {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
float lit = coverage + (1.0f - coverage) * level;
|
||||
return pack(color[0] * lit, color[1] * lit, color[2] * lit);
|
||||
}
|
||||
|
||||
private static void set(float[] out, int rgb) {
|
||||
out[0] = (rgb >> 16 & 0xFF) / 255.0f;
|
||||
out[1] = (rgb >> 8 & 0xFF) / 255.0f;
|
||||
out[2] = (rgb & 0xFF) / 255.0f;
|
||||
}
|
||||
|
||||
private static void mix(float[] out, int primary, int secondary, float t) {
|
||||
float[] a = new float[3];
|
||||
float[] b = new float[3];
|
||||
set(a, primary);
|
||||
set(b, secondary);
|
||||
for (int i = 0; i < 3; i++) out[i] = b[i] + t * (a[i] - b[i]);
|
||||
}
|
||||
|
||||
private static void lift(float[] color, float body, float extra) {
|
||||
for (int i = 0; i < 3; i++) color[i] = clamp(color[i] * body + extra);
|
||||
}
|
||||
|
||||
private static float clamp(float x) {
|
||||
return Math.max(0.0f, Math.min(1.0f, x));
|
||||
}
|
||||
|
||||
private static int pack(float r, float g, float b) {
|
||||
return Math.round(clamp(r) * 255.0f) << 16 | Math.round(clamp(g) * 255.0f) << 8 | Math.round(clamp(b) * 255.0f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dev.loki.lovisual.config.values.mode.BindMode;
|
||||
import dev.loki.lovisual.config.values.mode.ModeValue;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
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.modules.visuals.knife.anim.KnifeAnimator;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.model.KnifeModel;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.model.KnifeModels;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.render.KnifeRenderer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.tags.ItemTags;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
/**
|
||||
* Replaces the sword in your hand with an animated knife skin: first person view model with draw, inspect,
|
||||
* attack and run animations and your own arms, a resting knife in the third person hand, and the skin's sounds.
|
||||
* Hooks: KnifeItemInHandMixin, KnifeHandLayerMixin, KnifeInputMixin.
|
||||
*/
|
||||
@ModuleInfo(
|
||||
id = "knife",
|
||||
displayName = "Knife",
|
||||
aliases = {"knife", "meleeskins"},
|
||||
category = ModuleCategory.VISUALS
|
||||
)
|
||||
public final class Knife extends Module {
|
||||
private static final String ACTION_INSPECT = "inspect";
|
||||
private static final float MAX_DELTA_SECONDS = 0.25f;
|
||||
|
||||
private final ModeValue skin = modeSetting("knifeSkin", "skin", defaultSkin(), KnifeSkin.KEYS);
|
||||
private final BooleanValue firstPerson = bool("knifeFirstPerson", "first_person", true);
|
||||
private final BooleanValue thirdPerson = bool("knifeThirdPerson", "third_person", true);
|
||||
private final BooleanValue sounds = bool("knifeSounds", "sounds", true);
|
||||
private final NumberValue<Float> volume = visibleWhen(num("knifeVolume", "volume", 0.8f, 0.0f, 1.0f), sounds::get);
|
||||
|
||||
private final KnifeAnimator animator = new KnifeAnimator(KnifeSounds.INSTANCE, RandomGenerator.getDefault());
|
||||
private final KnifeRenderer renderer = new KnifeRenderer();
|
||||
private Item heldItem;
|
||||
private int heldSlot = -1;
|
||||
private long lastNanos;
|
||||
|
||||
{
|
||||
action(ACTION_INSPECT, "R", BindMode.PRESS);
|
||||
}
|
||||
|
||||
private static String defaultSkin() {
|
||||
return KnifeSkin.KEYS.length == 0 ? "" : java.util.Arrays.asList(KnifeSkin.KEYS).contains("karambit") ? "karambit" : KnifeSkin.KEYS[0];
|
||||
}
|
||||
|
||||
boolean soundsEnabled() {
|
||||
return sounds.get();
|
||||
}
|
||||
|
||||
float soundVolume() {
|
||||
return volume.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTick() {
|
||||
if (isActionPressedOnce(ACTION_INSPECT) && isEnabled()) animator.onInspect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
animator.reset();
|
||||
heldItem = null;
|
||||
heldSlot = -1;
|
||||
}
|
||||
|
||||
public static boolean isKnife(ItemStack stack) {
|
||||
return stack != null && stack.is(ItemTags.SWORDS);
|
||||
}
|
||||
|
||||
/** Draws the view model for the main hand; false leaves the hand to vanilla. */
|
||||
public boolean renderFirstPerson(AbstractClientPlayer player, InteractionHand hand, ItemStack item, PoseStack stack,
|
||||
SubmitNodeCollector collector, int light, float equipProgress) {
|
||||
if (!isEnabled() || !firstPerson.get() || hand != InteractionHand.MAIN_HAND || !isKnife(item)) return false;
|
||||
KnifeSkin current = KnifeSkin.byKey(skin.get());
|
||||
KnifeModel model = current == null ? null : KnifeModels.get(current.model());
|
||||
if (model == null) return false;
|
||||
|
||||
if (!animator.isBoundTo(current.key())) animator.bind(model, current.key(), current.variants());
|
||||
if (heldChanged(player)) animator.restart();
|
||||
animator.update(deltaSeconds(), new KnifeAnimator.Motion(player.isSprinting(), player.onGround(), player.moveDist));
|
||||
float retire = isKnife(player.getMainHandItem()) ? 0.0f : equipProgress;
|
||||
renderer.firstPerson(player, current, model, animator, stack, collector, light, retire);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Draws the resting knife in the third person hand of your own player. */
|
||||
public boolean renderThirdPerson(ItemStack item, HumanoidArm arm, PoseStack stack, SubmitNodeCollector collector, int light) {
|
||||
if (!isEnabled() || !thirdPerson.get() || !isKnife(item)) return false;
|
||||
KnifeSkin current = KnifeSkin.byKey(skin.get());
|
||||
KnifeModel model = current == null ? null : KnifeModels.get(current.model());
|
||||
if (model == null) return false;
|
||||
renderer.thirdPerson(current, model, arm, stack, collector, light);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** A click with the main hand holding a knife; the secondary click plays the third attack. */
|
||||
public void onAttack(boolean primary) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (isEnabled() && firstPerson.get() && mc.player != null && isKnife(mc.player.getMainHandItem())) {
|
||||
animator.onAttack(primary);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean heldChanged(AbstractClientPlayer player) {
|
||||
Item item = player.getMainHandItem().getItem();
|
||||
int slot = player.getInventory().getSelectedSlot();
|
||||
boolean changed = item != heldItem || slot != heldSlot;
|
||||
heldItem = item;
|
||||
heldSlot = slot;
|
||||
return changed;
|
||||
}
|
||||
|
||||
private float deltaSeconds() {
|
||||
long now = System.nanoTime();
|
||||
float delta = lastNanos == 0L ? 0.0f : (now - lastNanos) / 1.0E9f;
|
||||
lastNanos = now;
|
||||
return Minecraft.getInstance().isPaused() ? 0.0f : Math.max(0.0f, Math.min(delta, MAX_DELTA_SECONDS));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.anim.types.KnifeVariant;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* One entry of the knife catalog ({@code assets/lovisual/knife/index.json}). Recolours share the geometry
|
||||
* of {@code model}; {@code holyBones} get the holy shader; the transforms place the knife in a third person hand.
|
||||
*/
|
||||
public record KnifeSkin(String key, String name, String model, List<String> holyBones, List<KnifeVariant> variants,
|
||||
Transform thirdRight, Transform thirdLeft) {
|
||||
|
||||
/** Display-style placement in pixels and degrees. */
|
||||
public record Transform(float[] rotation, float[] translation, float[] scale) {
|
||||
public static final Transform NONE = new Transform(new float[3], new float[3], new float[]{1, 1, 1});
|
||||
}
|
||||
|
||||
private static final String INDEX = "/assets/lovisual/knife/index.json";
|
||||
public static final List<KnifeSkin> ALL = load();
|
||||
public static final String[] KEYS = ALL.stream().map(KnifeSkin::key).toArray(String[]::new);
|
||||
|
||||
public static KnifeSkin byKey(String key) {
|
||||
for (KnifeSkin skin : ALL) {
|
||||
if (skin.key.equals(key)) return skin;
|
||||
}
|
||||
return ALL.isEmpty() ? null : ALL.get(0);
|
||||
}
|
||||
|
||||
public Identifier texture() {
|
||||
return Identifier.fromNamespaceAndPath("lovisual", "textures/knife/" + key + ".png");
|
||||
}
|
||||
|
||||
static List<KnifeSkin> load() {
|
||||
try (InputStream in = KnifeSkin.class.getResourceAsStream(INDEX)) {
|
||||
return in == null ? List.of() : parse(new String(in.readAllBytes(), StandardCharsets.UTF_8));
|
||||
} catch (IOException | RuntimeException e) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
public static List<KnifeSkin> parse(String json) {
|
||||
List<KnifeSkin> out = new ArrayList<>();
|
||||
for (JsonElement entry : JsonParser.parseString(json).getAsJsonObject().getAsJsonArray("melee")) {
|
||||
JsonObject data = entry.getAsJsonObject();
|
||||
String key = data.get("key").getAsString();
|
||||
JsonObject transforms = data.has("transforms") ? data.getAsJsonObject("transforms") : new JsonObject();
|
||||
out.add(new KnifeSkin(key, data.get("name").getAsString(),
|
||||
data.has("model") ? data.get("model").getAsString() : key,
|
||||
strings(data.get("holyBones")), variants(data.get("variants")),
|
||||
transform(transforms.get("thirdperson_righthand")), transform(transforms.get("thirdperson_lefthand"))));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static List<KnifeVariant> variants(JsonElement element) {
|
||||
List<KnifeVariant> out = new ArrayList<>();
|
||||
if (element == null || !element.isJsonArray()) return out;
|
||||
for (JsonElement entry : element.getAsJsonArray()) {
|
||||
JsonObject data = entry.getAsJsonObject();
|
||||
out.add(new KnifeVariant(data.has("suffix") ? data.get("suffix").getAsString() : "", strings(data.get("hidden"))));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Transform transform(JsonElement element) {
|
||||
if (element == null || !element.isJsonObject()) return Transform.NONE;
|
||||
JsonObject data = element.getAsJsonObject();
|
||||
return new Transform(vec(data.get("rotation"), 0), vec(data.get("translation"), 0), vec(data.get("scale"), 1));
|
||||
}
|
||||
|
||||
private static float[] vec(JsonElement element, float fallback) {
|
||||
float[] out = {fallback, fallback, fallback};
|
||||
if (element == null || !element.isJsonArray()) return out;
|
||||
JsonArray array = element.getAsJsonArray();
|
||||
for (int i = 0; i < Math.min(3, array.size()); i++) out[i] = array.get(i).getAsFloat();
|
||||
return out;
|
||||
}
|
||||
|
||||
private static List<String> strings(JsonElement element) {
|
||||
List<String> out = new ArrayList<>();
|
||||
if (element == null || !element.isJsonArray()) return out;
|
||||
for (JsonElement entry : element.getAsJsonArray()) out.add(entry.getAsString());
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife;
|
||||
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.anim.types.KnifeSoundSink;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.client.resources.sounds.SimpleSoundInstance;
|
||||
import net.minecraft.client.resources.sounds.SoundInstance;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.util.RandomSource;
|
||||
|
||||
/** Plays the sound events of knife clips (registered in {@code assets/lovisual/sounds.json}) at the player. */
|
||||
final class KnifeSounds implements KnifeSoundSink {
|
||||
static final KnifeSounds INSTANCE = new KnifeSounds();
|
||||
|
||||
private KnifeSounds() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object play(String effect) {
|
||||
Knife knife = Modules.get(Knife.class);
|
||||
LocalPlayer player = Minecraft.getInstance().player;
|
||||
if (knife == null || player == null || !knife.soundsEnabled()) return null;
|
||||
SoundEvent event = SoundEvent.createVariableRangeEvent(Identifier.fromNamespaceAndPath("lovisual", effect));
|
||||
SimpleSoundInstance sound = new SimpleSoundInstance(event, SoundSource.PLAYERS, knife.soundVolume(), 1.0f,
|
||||
RandomSource.create(), player.getX(), player.getY(), player.getZ());
|
||||
Minecraft.getInstance().getSoundManager().play(sound);
|
||||
return sound;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean active(Object handle) {
|
||||
return handle instanceof SoundInstance sound && Minecraft.getInstance().getSoundManager().isActive(sound);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(Object handle) {
|
||||
if (handle instanceof SoundInstance sound) Minecraft.getInstance().getSoundManager().stop(sound);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife.anim;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.anim.types.ClipSource;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.anim.types.KnifeSoundSink;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.anim.types.KnifeVariant;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
/**
|
||||
* The view-model animation of one hand: a looping idle underneath, one-shot actions (draw, inspect, attacks) on
|
||||
* top of it, and an additive run cycle. Switching clips cross-fades from the pose that was showing.
|
||||
*/
|
||||
public final class KnifeAnimator {
|
||||
/** What the animator reads from the player each frame. */
|
||||
public record Motion(boolean sprinting, boolean onGround, float moveDist) {
|
||||
}
|
||||
|
||||
private static final float FADE_ACTION = 0.2f;
|
||||
private static final float FADE_RUN_START = 0.2f;
|
||||
private static final float FADE_RUN_LOOP = 0.2f;
|
||||
private static final float FADE_RUN_END = 0.3f;
|
||||
private static final float RUN_CYCLE_DISTANCE = 2.0f;
|
||||
private static final int PRIMARY_ATTACK_VARIANTS = 2;
|
||||
private static final int STRIDE = KnifeClip.STRIDE;
|
||||
|
||||
private enum Main {START, IDLE}
|
||||
|
||||
private enum Run {IDLE, RUNNING}
|
||||
|
||||
private final KnifeTrack base;
|
||||
private final KnifeTrack main;
|
||||
private final KnifeTrack movement;
|
||||
private final RandomGenerator random;
|
||||
|
||||
private ClipSource model;
|
||||
private String modelKey;
|
||||
private KnifeVariant variant = KnifeVariant.DEFAULT;
|
||||
private Main mainState = Main.START;
|
||||
private Run runState = Run.IDLE;
|
||||
private boolean runCycling;
|
||||
private float runAnchor;
|
||||
private int attackIndex;
|
||||
|
||||
private float[] staticPose = new float[0];
|
||||
private float[] additivePose = new float[0];
|
||||
private float[] staticFade = new float[0];
|
||||
private float[] additiveFade = new float[0];
|
||||
private float[] composed = new float[0];
|
||||
private boolean[] hidden = new boolean[0];
|
||||
private float staticFadeRemaining;
|
||||
private float staticFadeDuration;
|
||||
private float additiveFadeRemaining;
|
||||
private float additiveFadeDuration;
|
||||
|
||||
public KnifeAnimator(KnifeSoundSink sounds, RandomGenerator random) {
|
||||
this.base = new KnifeTrack(sounds);
|
||||
this.main = new KnifeTrack(sounds);
|
||||
this.movement = new KnifeTrack(sounds);
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
public boolean isBoundTo(String key) {
|
||||
return model != null && key.equals(modelKey);
|
||||
}
|
||||
|
||||
public float[] pose() {
|
||||
return composed;
|
||||
}
|
||||
|
||||
public boolean[] hidden() {
|
||||
return hidden;
|
||||
}
|
||||
|
||||
public String variantSuffix() {
|
||||
return variant.suffix();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
model = null;
|
||||
modelKey = null;
|
||||
base.clear();
|
||||
restart();
|
||||
}
|
||||
|
||||
/** Starts over with the draw clip, as after picking the knife up. */
|
||||
public void restart() {
|
||||
mainState = Main.START;
|
||||
runState = Run.IDLE;
|
||||
runCycling = false;
|
||||
attackIndex = 0;
|
||||
main.clear();
|
||||
movement.clear();
|
||||
staticFadeRemaining = 0.0f;
|
||||
additiveFadeRemaining = 0.0f;
|
||||
}
|
||||
|
||||
public void bind(ClipSource source, String key, List<KnifeVariant> variants) {
|
||||
model = source;
|
||||
modelKey = key;
|
||||
int size = source.boneCount() * STRIDE;
|
||||
staticPose = new float[size];
|
||||
additivePose = new float[size];
|
||||
staticFade = new float[size];
|
||||
additiveFade = new float[size];
|
||||
composed = new float[size];
|
||||
hidden = new boolean[source.boneCount()];
|
||||
variant = variants.isEmpty() ? KnifeVariant.DEFAULT : variants.get(random.nextInt(variants.size()));
|
||||
source.markBranches(variant.hidden(), hidden);
|
||||
restart();
|
||||
base.play(source.clip(named("static_idle")), KnifeTrack.Mode.LOOP);
|
||||
}
|
||||
|
||||
public void onAttack(boolean primary) {
|
||||
if (model == null || mainState != Main.IDLE) return;
|
||||
if (primary) {
|
||||
playMain(named("melee_" + (attackIndex + 1)), FADE_ACTION);
|
||||
attackIndex = (attackIndex + 1) % PRIMARY_ATTACK_VARIANTS;
|
||||
} else {
|
||||
playMain(named("melee_3"), FADE_ACTION);
|
||||
}
|
||||
}
|
||||
|
||||
public void onInspect() {
|
||||
if (model == null || mainState != Main.IDLE) return;
|
||||
String alternate = named("inspect_1");
|
||||
playMain(model.hasClip(alternate) && random.nextBoolean() ? alternate : named("inspect"), FADE_ACTION);
|
||||
}
|
||||
|
||||
/** Advances the clocks by {@code seconds} and recomposes the pose. */
|
||||
public void update(float seconds, Motion motion) {
|
||||
if (model == null) return;
|
||||
base.advance(seconds);
|
||||
main.advance(seconds);
|
||||
movement.advance(seconds);
|
||||
staticFadeRemaining = Math.max(0.0f, staticFadeRemaining - seconds);
|
||||
additiveFadeRemaining = Math.max(0.0f, additiveFadeRemaining - seconds);
|
||||
updateMain();
|
||||
updateMovement(motion);
|
||||
compose();
|
||||
}
|
||||
|
||||
private void updateMain() {
|
||||
if (mainState != Main.START) return;
|
||||
String alternate = named("draw_1");
|
||||
playMain(model.hasClip(alternate) && random.nextBoolean() ? alternate : named("draw"), 0.0f);
|
||||
mainState = Main.IDLE;
|
||||
}
|
||||
|
||||
private void updateMovement(Motion motion) {
|
||||
boolean canRun = model.hasClip(named("run_start")) && motion.sprinting() && motion.onGround() && main.isStopped();
|
||||
if (runState == Run.IDLE) {
|
||||
if (!canRun) return;
|
||||
runState = Run.RUNNING;
|
||||
runCycling = false;
|
||||
playMovement(named("run_start"), KnifeTrack.Mode.ONCE_HOLD, FADE_RUN_START);
|
||||
return;
|
||||
}
|
||||
if (!canRun) {
|
||||
runState = Run.IDLE;
|
||||
runCycling = false;
|
||||
playMovement(named("run_end"), KnifeTrack.Mode.ONCE_HOLD, FADE_RUN_END);
|
||||
return;
|
||||
}
|
||||
if (!runCycling) {
|
||||
if (!movement.isHolding()) return;
|
||||
runCycling = true;
|
||||
runAnchor = motion.moveDist();
|
||||
playMovement(named("run"), KnifeTrack.Mode.LOOP, FADE_RUN_LOOP);
|
||||
}
|
||||
float travelled = (motion.moveDist() - runAnchor) % RUN_CYCLE_DISTANCE;
|
||||
if (travelled < 0.0f) travelled += RUN_CYCLE_DISTANCE;
|
||||
movement.setProgress(travelled / RUN_CYCLE_DISTANCE);
|
||||
}
|
||||
|
||||
private void playMain(String clip, float fade) {
|
||||
KnifeClip next = model.clip(clip);
|
||||
if (next == null) return;
|
||||
beginStaticFade(fade);
|
||||
main.play(next, KnifeTrack.Mode.ONCE_STOP);
|
||||
}
|
||||
|
||||
private void playMovement(String clip, KnifeTrack.Mode mode, float fade) {
|
||||
KnifeClip next = model.clip(clip);
|
||||
if (next == null) {
|
||||
movement.clear();
|
||||
return;
|
||||
}
|
||||
beginAdditiveFade(fade);
|
||||
movement.play(next, mode);
|
||||
}
|
||||
|
||||
private void beginStaticFade(float duration) {
|
||||
if (duration <= 0.0f) {
|
||||
staticFadeRemaining = 0.0f;
|
||||
return;
|
||||
}
|
||||
System.arraycopy(staticPose, 0, staticFade, 0, staticPose.length);
|
||||
staticFadeDuration = duration;
|
||||
staticFadeRemaining = duration;
|
||||
}
|
||||
|
||||
private void beginAdditiveFade(float duration) {
|
||||
if (duration <= 0.0f) {
|
||||
additiveFadeRemaining = 0.0f;
|
||||
return;
|
||||
}
|
||||
System.arraycopy(additivePose, 0, additiveFade, 0, additivePose.length);
|
||||
additiveFadeDuration = duration;
|
||||
additiveFadeRemaining = duration;
|
||||
}
|
||||
|
||||
private void compose() {
|
||||
neutral(staticPose);
|
||||
base.evaluate(staticPose);
|
||||
if (main.isActive()) main.evaluate(staticPose);
|
||||
blend(staticPose, staticFade, staticFadeRemaining, staticFadeDuration);
|
||||
|
||||
neutral(additivePose);
|
||||
if (movement.isActive() || movement.isHolding()) movement.evaluate(additivePose);
|
||||
blend(additivePose, additiveFade, additiveFadeRemaining, additiveFadeDuration);
|
||||
|
||||
for (int at = 0; at + STRIDE <= composed.length; at += STRIDE) {
|
||||
for (int k = 0; k < 6; k++) composed[at + k] = staticPose[at + k] + additivePose[at + k];
|
||||
for (int k = 6; k < STRIDE; k++) composed[at + k] = staticPose[at + k] * additivePose[at + k];
|
||||
}
|
||||
}
|
||||
|
||||
static void neutral(float[] pose) {
|
||||
Arrays.fill(pose, 0.0f);
|
||||
for (int at = KnifeClip.SCALE; at + 3 <= pose.length; at += STRIDE) {
|
||||
pose[at] = 1.0f;
|
||||
pose[at + 1] = 1.0f;
|
||||
pose[at + 2] = 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pulls {@code pose} back towards {@code from} while a fade is running. */
|
||||
static void blend(float[] pose, float[] from, float remaining, float duration) {
|
||||
if (duration <= 0.0f || remaining <= 0.0f) return;
|
||||
float t = 1.0f - remaining / duration;
|
||||
for (int i = 0; i < pose.length; i++) pose[i] = from[i] + (pose[i] - from[i]) * t;
|
||||
}
|
||||
|
||||
private String named(String clip) {
|
||||
String suffixed = clip + variant.suffix();
|
||||
return !variant.suffix().isEmpty() && model != null && model.hasClip(suffixed) ? suffixed : clip;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife.anim;
|
||||
|
||||
import dev.loki.lovisual.util.bedrock.anim.Channel;
|
||||
import dev.loki.lovisual.util.bedrock.anim.Clip;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A clip bound to a model: bone names are resolved to indices once. A pose holds nine floats per bone:
|
||||
* rotation (degrees), position (pixels), scale.
|
||||
*/
|
||||
public final class KnifeClip {
|
||||
public static final int STRIDE = 9;
|
||||
public static final int POSITION = 3;
|
||||
public static final int SCALE = 6;
|
||||
|
||||
private final Clip clip;
|
||||
private final int[] bones;
|
||||
private final Clip.Tracks[] tracks;
|
||||
|
||||
public KnifeClip(Clip clip, Map<String, Integer> boneIndex) {
|
||||
this.clip = clip;
|
||||
int count = 0;
|
||||
int[] indices = new int[clip.bones().size()];
|
||||
Clip.Tracks[] resolved = new Clip.Tracks[indices.length];
|
||||
for (Map.Entry<String, Clip.Tracks> entry : clip.bones().entrySet()) {
|
||||
Integer index = boneIndex.get(entry.getKey());
|
||||
if (index == null) continue;
|
||||
indices[count] = index;
|
||||
resolved[count++] = entry.getValue();
|
||||
}
|
||||
this.bones = java.util.Arrays.copyOf(indices, count);
|
||||
this.tracks = java.util.Arrays.copyOf(resolved, count);
|
||||
}
|
||||
|
||||
public Clip clip() {
|
||||
return clip;
|
||||
}
|
||||
|
||||
public float length() {
|
||||
return clip.length();
|
||||
}
|
||||
|
||||
/** Overwrites the channels this clip animates; every other bone keeps what {@code pose} already holds. */
|
||||
public void evaluate(float time, float[] pose) {
|
||||
for (int i = 0; i < bones.length; i++) {
|
||||
int at = bones[i] * STRIDE;
|
||||
if (at + STRIDE > pose.length) continue;
|
||||
Clip.Tracks t = tracks[i];
|
||||
Channel rotation = t.rotation();
|
||||
Channel position = t.position();
|
||||
Channel scale = t.scale();
|
||||
if (rotation != null) rotation.sample(time, pose, at);
|
||||
if (position != null) position.sample(time, pose, at + POSITION);
|
||||
if (scale != null) scale.sample(time, pose, at + SCALE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife.anim;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.anim.types.KnifeSoundSink;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Plays one clip at a time: looping, once and stopping, or once and holding the last pose. */
|
||||
public final class KnifeTrack {
|
||||
public enum Mode {LOOP, ONCE_STOP, ONCE_HOLD}
|
||||
|
||||
private final KnifeSoundSink sounds;
|
||||
private final List<Object> playing = new ArrayList<>();
|
||||
private KnifeClip clip;
|
||||
private Mode mode = Mode.ONCE_STOP;
|
||||
private float time;
|
||||
private int soundCursor;
|
||||
private boolean stopped = true;
|
||||
|
||||
public KnifeTrack(KnifeSoundSink sounds) {
|
||||
this.sounds = sounds;
|
||||
}
|
||||
|
||||
public void play(KnifeClip next, Mode how) {
|
||||
silence();
|
||||
clip = next;
|
||||
mode = how;
|
||||
time = 0.0f;
|
||||
soundCursor = 0;
|
||||
stopped = next == null;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
play(null, Mode.ONCE_STOP);
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return clip != null && !stopped;
|
||||
}
|
||||
|
||||
/** True once a hold-mode clip has reached its end and keeps showing the last pose. */
|
||||
public boolean isHolding() {
|
||||
return clip != null && mode == Mode.ONCE_HOLD && time >= clip.length();
|
||||
}
|
||||
|
||||
public boolean isStopped() {
|
||||
return stopped || clip == null;
|
||||
}
|
||||
|
||||
public float time() {
|
||||
return time;
|
||||
}
|
||||
|
||||
/** Jumps to a fraction of the clip, used to tie a run cycle to the distance walked. */
|
||||
public void setProgress(float fraction) {
|
||||
if (clip != null && clip.length() > 0.0f) time = Math.max(0.0f, Math.min(fraction, 1.0f)) * clip.length();
|
||||
}
|
||||
|
||||
public void evaluate(float[] pose) {
|
||||
if (clip != null) clip.evaluate(time, pose);
|
||||
}
|
||||
|
||||
public void advance(float seconds) {
|
||||
if (clip == null || stopped) return;
|
||||
float length = clip.length();
|
||||
if (length <= 0.0f) {
|
||||
fire(0.0f);
|
||||
stopped = mode != Mode.ONCE_HOLD;
|
||||
return;
|
||||
}
|
||||
float next = time + seconds;
|
||||
if (next < length) {
|
||||
time = next;
|
||||
fire(next);
|
||||
return;
|
||||
}
|
||||
fire(length);
|
||||
switch (mode) {
|
||||
case LOOP -> {
|
||||
time = next % length;
|
||||
soundCursor = 0;
|
||||
fire(time);
|
||||
}
|
||||
case ONCE_HOLD -> time = length;
|
||||
case ONCE_STOP -> {
|
||||
time = length;
|
||||
stopped = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fire(float until) {
|
||||
if (clip == null) return;
|
||||
var source = clip.clip();
|
||||
while (soundCursor < source.soundCount() && source.soundTime(soundCursor) <= until) {
|
||||
Object handle = sounds.play(source.soundName(soundCursor));
|
||||
if (handle != null) {
|
||||
playing.removeIf(h -> !sounds.active(h));
|
||||
playing.add(handle);
|
||||
}
|
||||
soundCursor++;
|
||||
}
|
||||
}
|
||||
|
||||
private void silence() {
|
||||
for (Object handle : playing) sounds.stop(handle);
|
||||
playing.clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife.anim.types;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.anim.KnifeClip;
|
||||
|
||||
/** What the animator needs from a model: its clips by name and its bone count. */
|
||||
public interface ClipSource {
|
||||
KnifeClip clip(String name);
|
||||
|
||||
int boneCount();
|
||||
|
||||
default boolean hasClip(String name) {
|
||||
return clip(name) != null;
|
||||
}
|
||||
|
||||
/** Marks the named bones and everything below them in {@code flags}. */
|
||||
void markBranches(java.util.List<String> names, boolean[] flags);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife.anim.types;
|
||||
|
||||
/** Where animation sound events go; handles let a track silence what it started when its clip is replaced. */
|
||||
public interface KnifeSoundSink {
|
||||
KnifeSoundSink SILENT = new KnifeSoundSink() {
|
||||
@Override
|
||||
public Object play(String effect) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean active(Object handle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(Object handle) {
|
||||
}
|
||||
};
|
||||
|
||||
/** Starts the sound event and returns a handle, or null when nothing plays. */
|
||||
Object play(String effect);
|
||||
|
||||
boolean active(Object handle);
|
||||
|
||||
void stop(Object handle);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife.anim.types;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** A look of one skin: clips carry {@code suffix} and the {@code hidden} bone branches are not drawn. */
|
||||
public record KnifeVariant(String suffix, List<String> hidden) {
|
||||
public static final KnifeVariant DEFAULT = new KnifeVariant("", List.of());
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife.model;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.anim.KnifeClip;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.anim.types.ClipSource;
|
||||
import dev.loki.lovisual.util.bedrock.anim.Clip;
|
||||
import dev.loki.lovisual.util.bedrock.geo.GeoBone;
|
||||
import dev.loki.lovisual.util.bedrock.geo.GeoCube;
|
||||
import dev.loki.lovisual.util.bedrock.geo.GeoParser;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A knife view model ready to draw: bones in parent-first order with pivots (pixels, entity frame) and rest
|
||||
* rotations, the geometry of every bone baked to quads in blocks, and the model's clips. Special bones are
|
||||
* found by name: {@code righthand_pos}, {@code lefthand_pos}, {@code camera}, {@code idle_view} and
|
||||
* {@code thirdperson_hand}.
|
||||
*/
|
||||
public final class KnifeModel implements ClipSource {
|
||||
public static final int FLOATS_PER_VERTEX = 8;
|
||||
public static final int VERTICES_PER_FACE = 4;
|
||||
|
||||
private final String[] names;
|
||||
private final int[] parents;
|
||||
private final float[] pivots;
|
||||
private final float[] rest;
|
||||
private final float[][] meshes;
|
||||
private final Map<String, Integer> index;
|
||||
private final Map<String, KnifeClip> clips;
|
||||
|
||||
private KnifeModel(String[] names, int[] parents, float[] pivots, float[] rest, float[][] meshes,
|
||||
Map<String, Integer> index, Map<String, KnifeClip> clips) {
|
||||
this.names = names;
|
||||
this.parents = parents;
|
||||
this.pivots = pivots;
|
||||
this.rest = rest;
|
||||
this.meshes = meshes;
|
||||
this.index = index;
|
||||
this.clips = clips;
|
||||
}
|
||||
|
||||
public static KnifeModel bake(String geometryJson, String animationJson) {
|
||||
GeoParser.Model geo = GeoParser.parse(geometryJson);
|
||||
List<GeoBone> ordered = new ArrayList<>();
|
||||
for (GeoBone root : geo.roots()) collect(root, ordered);
|
||||
|
||||
int count = ordered.size();
|
||||
Map<String, Integer> index = new HashMap<>();
|
||||
for (int i = 0; i < count; i++) index.put(ordered.get(i).name, i);
|
||||
Map<GeoBone, GeoBone> parentOf = new HashMap<>();
|
||||
for (GeoBone bone : ordered) {
|
||||
for (GeoBone child : bone.children) parentOf.put(child, bone);
|
||||
}
|
||||
|
||||
String[] names = new String[count];
|
||||
int[] parents = new int[count];
|
||||
float[] pivots = new float[count * 3];
|
||||
float[] rest = new float[count * 3];
|
||||
float[][] meshes = new float[count][];
|
||||
for (int i = 0; i < count; i++) {
|
||||
GeoBone bone = ordered.get(i);
|
||||
names[i] = bone.name;
|
||||
GeoBone parent = parentOf.get(bone);
|
||||
parents[i] = parent == null ? -1 : index.get(parent.name);
|
||||
System.arraycopy(bone.pivot, 0, pivots, i * 3, 3);
|
||||
System.arraycopy(bone.baseRot, 0, rest, i * 3, 3);
|
||||
meshes[i] = mesh(bone, geo.textureWidth(), geo.textureHeight());
|
||||
}
|
||||
Map<String, KnifeClip> clips = new HashMap<>();
|
||||
for (Map.Entry<String, Clip> entry : Clip.parseAll(animationJson).entrySet()) {
|
||||
clips.put(entry.getKey(), new KnifeClip(entry.getValue(), index));
|
||||
}
|
||||
return new KnifeModel(names, parents, pivots, rest, meshes, Map.copyOf(index), clips);
|
||||
}
|
||||
|
||||
private static void collect(GeoBone bone, List<GeoBone> out) {
|
||||
out.add(bone);
|
||||
for (GeoBone child : bone.children) collect(child, out);
|
||||
}
|
||||
|
||||
/** Quads in blocks, each vertex: position (3), uv (2), normal (3); the cube's own rotation is baked in. */
|
||||
private static float[] mesh(GeoBone bone, int textureWidth, int textureHeight) {
|
||||
int faces = 0;
|
||||
for (GeoCube cube : bone.cubes) faces += cube.faces().size();
|
||||
if (faces == 0) return null;
|
||||
float[] out = new float[faces * VERTICES_PER_FACE * FLOATS_PER_VERTEX];
|
||||
int at = 0;
|
||||
Matrix4f matrix = new Matrix4f();
|
||||
Vector3f[] corner = {new Vector3f(), new Vector3f(), new Vector3f(), new Vector3f()};
|
||||
Vector3f edgeRight = new Vector3f();
|
||||
Vector3f edgeDown = new Vector3f();
|
||||
for (GeoCube cube : bone.cubes) {
|
||||
cube.transform(matrix.identity());
|
||||
for (float[] face : cube.faces()) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
corner[i].set(face[i * 5] / 16.0f, face[i * 5 + 1] / 16.0f, face[i * 5 + 2] / 16.0f);
|
||||
matrix.transformPosition(corner[i]);
|
||||
}
|
||||
corner[1].sub(corner[0], edgeRight);
|
||||
corner[3].sub(corner[0], edgeDown);
|
||||
Vector3f normal = edgeDown.cross(edgeRight, new Vector3f());
|
||||
if (normal.lengthSquared() > 0.0f) normal.normalize();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
out[at++] = corner[i].x;
|
||||
out[at++] = corner[i].y;
|
||||
out[at++] = corner[i].z;
|
||||
out[at++] = face[i * 5 + 3] / textureWidth;
|
||||
out[at++] = face[i * 5 + 4] / textureHeight;
|
||||
out[at++] = normal.x;
|
||||
out[at++] = normal.y;
|
||||
out[at++] = normal.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int boneCount() {
|
||||
return names.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KnifeClip clip(String name) {
|
||||
return clips.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markBranches(List<String> boneNames, boolean[] flags) {
|
||||
java.util.Arrays.fill(flags, false);
|
||||
for (String name : boneNames) {
|
||||
Integer bone = index.get(name);
|
||||
if (bone != null) flags[bone] = true;
|
||||
}
|
||||
for (int i = 0; i < flags.length; i++) {
|
||||
if (parents[i] >= 0 && flags[parents[i]]) flags[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
public String name(int bone) {
|
||||
return names[bone];
|
||||
}
|
||||
|
||||
public int parentOf(int bone) {
|
||||
return parents[bone];
|
||||
}
|
||||
|
||||
public float pivotX(int bone) {
|
||||
return pivots[bone * 3];
|
||||
}
|
||||
|
||||
public float pivotY(int bone) {
|
||||
return pivots[bone * 3 + 1];
|
||||
}
|
||||
|
||||
public float pivotZ(int bone) {
|
||||
return pivots[bone * 3 + 2];
|
||||
}
|
||||
|
||||
public float restX(int bone) {
|
||||
return rest[bone * 3];
|
||||
}
|
||||
|
||||
public float restY(int bone) {
|
||||
return rest[bone * 3 + 1];
|
||||
}
|
||||
|
||||
public float restZ(int bone) {
|
||||
return rest[bone * 3 + 2];
|
||||
}
|
||||
|
||||
/** Quads of one bone, or null when it has no geometry. */
|
||||
public float[] meshOf(int bone) {
|
||||
return meshes[bone];
|
||||
}
|
||||
|
||||
public int bone(String name) {
|
||||
return index.getOrDefault(name, -1);
|
||||
}
|
||||
|
||||
public int rightHand() {
|
||||
return bone("righthand_pos");
|
||||
}
|
||||
|
||||
public int leftHand() {
|
||||
return bone("lefthand_pos");
|
||||
}
|
||||
|
||||
public int camera() {
|
||||
return bone("camera");
|
||||
}
|
||||
|
||||
/** The bone that sits at the player's eye; falls back to the camera bone like the reference does. */
|
||||
public int viewAnchor() {
|
||||
int anchor = bone("idle_view");
|
||||
return anchor >= 0 ? anchor : camera();
|
||||
}
|
||||
|
||||
public int thirdPersonHand() {
|
||||
return bone("thirdperson_hand");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife.model;
|
||||
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/** Loads and bakes knife models on a background thread; {@link #get} returns null until a model is ready. */
|
||||
public final class KnifeModels {
|
||||
private static final String ROOT = "/assets/lovisual/knife/";
|
||||
private static final Map<String, KnifeModel> READY = new ConcurrentHashMap<>();
|
||||
private static final Set<String> PENDING = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<String> FAILED = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private KnifeModels() {
|
||||
}
|
||||
|
||||
public static KnifeModel get(String modelKey) {
|
||||
KnifeModel ready = READY.get(modelKey);
|
||||
if (ready != null || FAILED.contains(modelKey)) return ready;
|
||||
if (PENDING.add(modelKey)) {
|
||||
Thread loader = new Thread(() -> load(modelKey), "lovisual-knife-loader");
|
||||
loader.setDaemon(true);
|
||||
loader.start();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
READY.clear();
|
||||
FAILED.clear();
|
||||
}
|
||||
|
||||
private static void load(String modelKey) {
|
||||
try {
|
||||
READY.put(modelKey, KnifeModel.bake(read(modelKey + ".geo.json"), read(modelKey + ".anim.json")));
|
||||
} catch (IOException | RuntimeException e) {
|
||||
FAILED.add(modelKey);
|
||||
DebugLog.warn("[LoVisual] Knife model '" + modelKey + "' failed to load: " + e);
|
||||
} finally {
|
||||
PENDING.remove(modelKey);
|
||||
}
|
||||
}
|
||||
|
||||
private static String read(String file) throws IOException {
|
||||
try (InputStream in = KnifeModels.class.getResourceAsStream(ROOT + file)) {
|
||||
if (in == null) throw new IOException("missing resource " + file);
|
||||
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife.render;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.anim.KnifeClip;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.model.KnifeModel;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Quaternionf;
|
||||
|
||||
/** World matrices of a posed knife model, one per bone, plus which bones are drawn. */
|
||||
final class KnifeBones {
|
||||
private static final float DEGREES = (float) (Math.PI / 180.0);
|
||||
|
||||
Matrix4f[] world = new Matrix4f[0];
|
||||
boolean[] visible = new boolean[0];
|
||||
private final Matrix4f local = new Matrix4f();
|
||||
private final Quaternionf rotation = new Quaternionf();
|
||||
|
||||
/**
|
||||
* Poses every bone. {@code pose} holds nine floats per bone (see {@link KnifeClip}); a null pose is the
|
||||
* model at rest. Bones flagged in {@code hidden} and the two hand marker bones are not drawn.
|
||||
*/
|
||||
void build(KnifeModel model, float[] pose, boolean[] hidden) {
|
||||
int count = model.boneCount();
|
||||
ensure(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
visible[i] = hidden == null || i >= hidden.length || !hidden[i];
|
||||
int at = i * KnifeClip.STRIDE;
|
||||
boolean posed = pose != null && at + KnifeClip.STRIDE <= pose.length;
|
||||
float rx = model.restX(i) - (posed ? pose[at] * DEGREES : 0.0f);
|
||||
float ry = model.restY(i) - (posed ? pose[at + 1] * DEGREES : 0.0f);
|
||||
float rz = model.restZ(i) + (posed ? pose[at + 2] * DEGREES : 0.0f);
|
||||
|
||||
local.identity();
|
||||
if (posed) local.translate(-pose[at + 3] / 16.0f, pose[at + 4] / 16.0f, pose[at + 5] / 16.0f);
|
||||
float px = model.pivotX(i) / 16.0f;
|
||||
float py = model.pivotY(i) / 16.0f;
|
||||
float pz = model.pivotZ(i) / 16.0f;
|
||||
local.translate(px, py, pz);
|
||||
local.rotate(rotation.rotationZ(rz).rotateY(ry).rotateX(rx));
|
||||
if (posed) local.scale(pose[at + 6], pose[at + 7], pose[at + 8]);
|
||||
local.translate(-px, -py, -pz);
|
||||
|
||||
int parent = model.parentOf(i);
|
||||
if (parent < 0) world[i].set(local);
|
||||
else world[i].set(world[parent]).mul(local);
|
||||
}
|
||||
hideHands(model);
|
||||
}
|
||||
|
||||
private void hideHands(KnifeModel model) {
|
||||
int right = model.rightHand();
|
||||
int left = model.leftHand();
|
||||
if (right >= 0) visible[right] = false;
|
||||
if (left >= 0) visible[left] = false;
|
||||
}
|
||||
|
||||
private void ensure(int count) {
|
||||
if (world.length >= count) return;
|
||||
Matrix4f[] grown = new Matrix4f[count];
|
||||
System.arraycopy(world, 0, grown, 0, world.length);
|
||||
for (int i = world.length; i < count; i++) grown[i] = new Matrix4f();
|
||||
world = grown;
|
||||
visible = new boolean[count];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.model.KnifeModel;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
/** Sends the posed bones of a knife model to the renderer as one custom-geometry layer. */
|
||||
final class KnifeGeometry {
|
||||
private KnifeGeometry() {
|
||||
}
|
||||
|
||||
static void submit(KnifeBones bones, KnifeModel model, Identifier texture, PoseStack stack,
|
||||
SubmitNodeCollector collector, int light) {
|
||||
int count = model.boneCount();
|
||||
collector.submitCustomGeometry(stack, RenderTypes.entityTranslucent(texture), (pose, consumer) -> {
|
||||
Matrix3f normalMatrix = new Matrix3f();
|
||||
Vector3f point = new Vector3f();
|
||||
Vector3f normal = new Vector3f();
|
||||
for (int bone = 0; bone < count; bone++) {
|
||||
float[] mesh = model.meshOf(bone);
|
||||
if (mesh == null || !bones.visible[bone]) continue;
|
||||
Matrix4f world = bones.world[bone];
|
||||
world.normal(normalMatrix);
|
||||
for (int v = 0; v + KnifeModel.FLOATS_PER_VERTEX <= mesh.length; v += KnifeModel.FLOATS_PER_VERTEX) {
|
||||
point.set(mesh[v], mesh[v + 1], mesh[v + 2]);
|
||||
world.transformPosition(point);
|
||||
normal.set(mesh[v + 5], mesh[v + 6], mesh[v + 7]).mul(normalMatrix).normalize();
|
||||
vertex(pose, consumer, point, normal, mesh[v + 3], mesh[v + 4], light);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void vertex(PoseStack.Pose pose, VertexConsumer consumer, Vector3f point, Vector3f normal,
|
||||
float u, float v, int light) {
|
||||
consumer.addVertex(pose, point.x, point.y, point.z)
|
||||
.setColor(-1).setUv(u, v)
|
||||
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(light)
|
||||
.setNormal(pose, normal.x, normal.y, normal.z);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.knife.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.KnifeSkin;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.anim.KnifeAnimator;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.anim.KnifeClip;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.model.KnifeModel;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.entity.player.AvatarRenderer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.entity.player.PlayerModelPart;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/**
|
||||
* Draws a knife skin as the first person view model (with the player's own arms attached at the model's hand
|
||||
* markers) and in a third person hand. The pose stack is expected at the camera, like vanilla's hand rendering.
|
||||
*/
|
||||
public final class KnifeRenderer {
|
||||
private static final float EQUIP_DROP = -0.6f;
|
||||
private static final float ARM_ALIGN_X = 0.0625f;
|
||||
private static final float ARM_ALIGN_Y = 0.5f;
|
||||
private static final float THIRD_HAND_X = 1.0f / 16.0f;
|
||||
private static final float THIRD_HAND_Y = 0.125f;
|
||||
private static final float THIRD_HAND_Z = -0.625f;
|
||||
|
||||
private final KnifeBones primary = new KnifeBones();
|
||||
private final KnifeBones rest = new KnifeBones();
|
||||
private final Matrix4f armPose = new Matrix4f();
|
||||
|
||||
public void firstPerson(AbstractClientPlayer player, KnifeSkin skin, KnifeModel model, KnifeAnimator animator,
|
||||
PoseStack stack, SubmitNodeCollector collector, int light, float retire) {
|
||||
float[] pose = animator.pose();
|
||||
boolean mirrored = player.getMainArm() == HumanoidArm.LEFT;
|
||||
stack.pushPose();
|
||||
stack.translate(0.0f, retire * EQUIP_DROP, 0.0f);
|
||||
if (mirrored) stack.scale(-1.0f, 1.0f, 1.0f);
|
||||
applyCamera(model, pose, stack);
|
||||
applyViewAnchor(model, stack);
|
||||
primary.build(model, pose, animator.hidden());
|
||||
KnifeGeometry.submit(primary, model, skin.texture(), stack, collector, light);
|
||||
arm(model.rightHand(), true, mirrored, player, stack, collector, light);
|
||||
arm(model.leftHand(), false, mirrored, player, stack, collector, light);
|
||||
stack.popPose();
|
||||
}
|
||||
|
||||
/** Draws the resting knife in the hand the stack has been moved to (see KnifeHandLayerMixin). */
|
||||
public void thirdPerson(KnifeSkin skin, KnifeModel model, HumanoidArm arm, PoseStack stack,
|
||||
SubmitNodeCollector collector, int light) {
|
||||
KnifeSkin.Transform place = arm == HumanoidArm.RIGHT ? skin.thirdRight() : skin.thirdLeft();
|
||||
stack.pushPose();
|
||||
stack.translate((arm == HumanoidArm.LEFT ? -1.0f : 1.0f) * THIRD_HAND_X, THIRD_HAND_Y, THIRD_HAND_Z);
|
||||
stack.translate(place.translation()[0] / 16.0f, place.translation()[1] / 16.0f, place.translation()[2] / 16.0f);
|
||||
stack.mulPose(Axis.XP.rotationDegrees(place.rotation()[0]));
|
||||
stack.mulPose(Axis.YP.rotationDegrees(place.rotation()[1]));
|
||||
stack.mulPose(Axis.ZP.rotationDegrees(place.rotation()[2]));
|
||||
stack.scale(place.scale()[0], place.scale()[1], place.scale()[2]);
|
||||
stack.mulPose(Axis.ZP.rotationDegrees(180.0f));
|
||||
int hand = model.thirdPersonHand();
|
||||
if (hand >= 0) stack.translate(-model.pivotX(hand) / 16.0f, -model.pivotY(hand) / 16.0f, -model.pivotZ(hand) / 16.0f);
|
||||
rest.build(model, null, null);
|
||||
KnifeGeometry.submit(rest, model, skin.texture(), stack, collector, light);
|
||||
stack.popPose();
|
||||
}
|
||||
|
||||
/** The model's camera bone shakes the view during inspects and attacks. */
|
||||
private static void applyCamera(KnifeModel model, float[] pose, PoseStack stack) {
|
||||
int camera = model.camera();
|
||||
int at = camera * KnifeClip.STRIDE;
|
||||
if (camera < 0 || at + KnifeClip.STRIDE > pose.length) return;
|
||||
stack.translate(pose[at + 3] / 16.0f, -pose[at + 4] / 16.0f, -pose[at + 5] / 16.0f);
|
||||
stack.mulPose(Axis.ZP.rotationDegrees(-pose[at + 2]));
|
||||
stack.mulPose(Axis.YP.rotationDegrees(pose[at + 1]));
|
||||
stack.mulPose(Axis.XP.rotationDegrees(pose[at]));
|
||||
}
|
||||
|
||||
private static void applyViewAnchor(KnifeModel model, PoseStack stack) {
|
||||
int anchor = model.viewAnchor();
|
||||
if (anchor < 0) return;
|
||||
stack.translate(-model.pivotX(anchor) / 16.0f, -model.pivotY(anchor) / 16.0f, -model.pivotZ(anchor) / 16.0f);
|
||||
}
|
||||
|
||||
private void arm(int bone, boolean right, boolean mirrored, AbstractClientPlayer player, PoseStack stack,
|
||||
SubmitNodeCollector collector, int light) {
|
||||
if (bone < 0) return;
|
||||
AvatarRenderer<?> renderer = Minecraft.getInstance().getEntityRenderDispatcher().getPlayerRenderer(player);
|
||||
if (renderer == null) return;
|
||||
Identifier skin = player.getSkin().body().texturePath();
|
||||
armPose.set(primary.world[bone]).translate(right ? -ARM_ALIGN_X : ARM_ALIGN_X, ARM_ALIGN_Y, 0.0f);
|
||||
stack.pushPose();
|
||||
stack.mulPose(armPose);
|
||||
if (mirrored) stack.scale(-1.0f, 1.0f, 1.0f);
|
||||
if (right) renderer.renderRightHand(stack, collector, light, skin, player.isModelPartShown(PlayerModelPart.RIGHT_SLEEVE));
|
||||
else renderer.renderLeftHand(stack, collector, light, skin, player.isModelPartShown(PlayerModelPart.LEFT_SLEEVE));
|
||||
stack.popPose();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,9 @@ import dev.loki.lovisual.config.values.primitive.NumberValue;
|
|||
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.modules.visuals.wings.model.WingModelDef;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.wings.model.WingModelRenderer;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.wings.model.WingModels;
|
||||
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||
import dev.loki.lovisual.features.relations.PlayerRelations;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
|
|
@ -19,7 +22,8 @@ import net.minecraft.world.item.Items;
|
|||
/**
|
||||
* Cosmetic wings on player backs (ported from the WiNGS reference): 8 silhouette types,
|
||||
* pose-driven flapping (idle/sneak/glide), friends/all-players visibility. Geometry data
|
||||
* lives in {@link WingShapes}, mesh building in {@link WingsRenderer}.
|
||||
* lives in {@link WingShapes}, mesh building in {@link WingsRenderer}. The "Model" source draws one of
|
||||
* 14 textured Bedrock models instead (package {@code model}); colour then tints the texture.
|
||||
*/
|
||||
@ModuleInfo(
|
||||
id = "wings",
|
||||
|
|
@ -29,23 +33,27 @@ import net.minecraft.world.item.Items;
|
|||
)
|
||||
public final class Wings extends Module {
|
||||
|
||||
private final ModeValue wingType = modeSetting(
|
||||
"wingsType", "type", "Angelic", WingShapes.TYPES);
|
||||
private final ModeValue source = modeSetting(
|
||||
"wingsSource", "source", "Procedural", "Procedural", "Model");
|
||||
private final ModeValue wingModel = visibleWhen(modeSetting(
|
||||
"wingsModel", "model", "Angel", WingModelDef.NAMES), this::usesModel);
|
||||
private final ModeValue wingType = visibleWhen(modeSetting(
|
||||
"wingsType", "type", "Angelic", WingShapes.TYPES), () -> !usesModel());
|
||||
private final ModeValue target = modeSetting(
|
||||
"wingsTarget", "target", "Self", "Self", "Friends", "All Players");
|
||||
private final NumberValue<Float> scale =
|
||||
num("wingsScale", "scale", 1.0f, 0.3f, 3.0f);
|
||||
private final BooleanValue flapping =
|
||||
bool("wingsFlapping", "flapping", true);
|
||||
visibleWhen(bool("wingsFlapping", "flapping", true), () -> !usesModel());
|
||||
private final NumberValue<Float> flapStrength =
|
||||
visibleWhen(num("wingsFlapStrength", "flap_strength", 30.0f, 5.0f, 60.0f), flapping::get);
|
||||
visibleWhen(num("wingsFlapStrength", "flap_strength", 30.0f, 5.0f, 60.0f), () -> !usesModel() && flapping.get());
|
||||
private final NumberValue<Float> flapSpeed =
|
||||
visibleWhen(num("wingsFlapSpeed", "flap_speed", 3.0f, 0.5f, 8.0f), flapping::get);
|
||||
visibleWhen(num("wingsFlapSpeed", "flap_speed", 3.0f, 0.5f, 8.0f), () -> !usesModel() && flapping.get());
|
||||
private final BooleanValue throughWalls =
|
||||
bool("wingsThroughWalls", "through_walls", false);
|
||||
private final BooleanValue glow = bool("wingsGlow", "glow", true);
|
||||
private final BooleanValue outline = bool("wingsOutline", "outline", true);
|
||||
private final BooleanValue ribs = bool("wingsRibs", "ribs", true);
|
||||
private final BooleanValue glow = visibleWhen(bool("wingsGlow", "glow", true), () -> !usesModel());
|
||||
private final BooleanValue outline = visibleWhen(bool("wingsOutline", "outline", true), () -> !usesModel());
|
||||
private final BooleanValue ribs = visibleWhen(bool("wingsRibs", "ribs", true), () -> !usesModel());
|
||||
private final RGBAColorValue color =
|
||||
color("wingsColor", "color", "#FFFFFFDC");
|
||||
|
||||
|
|
@ -57,11 +65,20 @@ public final class Wings extends Module {
|
|||
return WorldPhase.END_MAIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
WingModels.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
selfYawReady = false;
|
||||
}
|
||||
|
||||
private boolean usesModel() {
|
||||
return "Model".equals(source.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
|
|
@ -71,21 +88,26 @@ public final class Wings extends Module {
|
|||
wingType.get(), scale.get(), flapping.get(), flapStrength.get(), flapSpeed.get(),
|
||||
color.getArgb(), glow.get(), outline.get(), ribs.get(), !throughWalls.get());
|
||||
|
||||
WingModels.Loaded model = usesModel() ? WingModels.get(WingModelDef.byName(wingModel.get())) : null;
|
||||
if (usesModel() && model == null) return;
|
||||
WingModelRenderer.Style modelStyle = new WingModelRenderer.Style(
|
||||
scale.get(), color.getArgb() & 0xFFFFFF, !throughWalls.get());
|
||||
|
||||
if (showsSelf() && !mc.options.getCameraType().isFirstPerson()
|
||||
&& mc.player.isAlive() && !hasElytra(mc.player)) {
|
||||
renderOne(renderer, mc.player, tickDelta, style, true);
|
||||
renderOne(renderer, mc.player, tickDelta, style, model, modelStyle, true);
|
||||
}
|
||||
if (showsOthers()) {
|
||||
for (Player player : mc.level.players()) {
|
||||
if (player == mc.player || !player.isAlive() || hasElytra(player)) continue;
|
||||
if (!showsOthers(player)) continue;
|
||||
renderOne(renderer, player, tickDelta, style, false);
|
||||
renderOne(renderer, player, tickDelta, style, model, modelStyle, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void renderOne(Renderer3D renderer, Player player, float tickDelta,
|
||||
WingsRenderer.Style style, boolean isSelf) {
|
||||
private void renderOne(Renderer3D renderer, Player player, float tickDelta, WingsRenderer.Style style,
|
||||
WingModels.Loaded model, WingModelRenderer.Style modelStyle, boolean isSelf) {
|
||||
float targetYaw = Mth.rotLerp(tickDelta, player.yBodyRotO, player.yBodyRot);
|
||||
float bodyYaw;
|
||||
if (isSelf) {
|
||||
|
|
@ -99,7 +121,11 @@ public final class Wings extends Module {
|
|||
} else {
|
||||
bodyYaw = targetYaw;
|
||||
}
|
||||
WingsRenderer.render(renderer, player, tickDelta, bodyYaw, style);
|
||||
if (model != null) {
|
||||
WingModelRenderer.render(renderer, player, tickDelta, bodyYaw, model, modelStyle);
|
||||
} else {
|
||||
WingsRenderer.render(renderer, player, tickDelta, bodyYaw, style);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean showsSelf() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.wings.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* One entry of the wing model catalog. {@code animation} names an {@code .animation.json} next to the model, or
|
||||
* is one of the procedural ids ("angel", "demon", "dragon"). {@code scale}, {@code offsetY} and {@code offsetZ}
|
||||
* place the model on the back; {@code frames} is the number of texture frames stacked vertically.
|
||||
*/
|
||||
public record WingModelDef(String name, String geometry, String texture, String animation,
|
||||
float scale, float offsetY, float offsetZ, int frames) {
|
||||
|
||||
public static final List<WingModelDef> ALL = List.of(
|
||||
new WingModelDef("Baby Dragon", "dragon", "baby_dragon_white", "dragon", 1.0f, 0.45f, 0.0f, 1),
|
||||
new WingModelDef("Angel", "angel", "angel_white", "angel", 1.0f, 0.45f, 0.0f, 1),
|
||||
new WingModelDef("Demon Red", "demon", "demon_red", "demon", 1.0f, 0.45f, 0.0f, 1),
|
||||
new WingModelDef("Demon Black", "demon", "demon_black", "demon", 1.0f, 0.45f, 0.0f, 1),
|
||||
new WingModelDef("Pulse", "pulse", "pulse", "pulse", 1.0f, -1.2f, 0.0f, 1),
|
||||
new WingModelDef("Dark", "dark", "dark", "dark", 0.9f, -1.05f, 0.05f, 1),
|
||||
new WingModelDef("Ghoul", "ghoul", "ghoul", "ghoul", 0.6f, -0.65f, 0.0f, 1),
|
||||
new WingModelDef("Rocker", "rocker", "rocker", "rocker", 0.9f, -1.05f, 0.0f, 1),
|
||||
new WingModelDef("Wota", "wota", "wota", "wota", 0.9f, -1.05f, 0.0f, 10),
|
||||
new WingModelDef("Trident", "trident", "trident", "trident", 0.9f, -1.05f, 0.2f, 1),
|
||||
new WingModelDef("Easter", "easter", "easter", "easter", 0.9f, -1.05f, 0.0f, 1),
|
||||
new WingModelDef("Blue Dragon", "blue_dragon", "blue_dragon", "blue_dragon", 0.9f, -1.05f, 0.0f, 1),
|
||||
new WingModelDef("Spider", "spider", "spider", "spider", 1.0f, 0.0f, 0.0f, 1),
|
||||
new WingModelDef("Vulcano", "vulcano", "vulcano", "vulcano", 1.0f, 0.0f, 0.0f, 1));
|
||||
|
||||
public static final String[] NAMES = ALL.stream().map(WingModelDef::name).toArray(String[]::new);
|
||||
|
||||
public static WingModelDef byName(String name) {
|
||||
for (WingModelDef def : ALL) {
|
||||
if (def.name.equals(name)) return def;
|
||||
}
|
||||
return ALL.get(0);
|
||||
}
|
||||
|
||||
public boolean procedural() {
|
||||
return animation.equals("angel") || animation.equals("demon") || animation.equals("dragon");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.wings.model;
|
||||
|
||||
import dev.loki.lovisual.util.bedrock.geo.GeoBone;
|
||||
import dev.loki.lovisual.util.bedrock.geo.GeoCube;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Draws a loaded wing model on a player's back. The anchor chain follows the vanilla player model (body
|
||||
* pivot, crouch lean) and is then moved by the catalog offsets; the bones are posed by the model's animator.
|
||||
*/
|
||||
public final class WingModelRenderer {
|
||||
private static final float MODEL_HEIGHT = 1.501f;
|
||||
private static final float ATTACHMENT_SHIFT = -0.3f;
|
||||
private static final float CHEST_ARMOR_OFFSET = 0.078125f;
|
||||
private static final float CROUCH_BODY_DROP = 0.2f;
|
||||
private static final float CROUCH_LEAN = 0.5f;
|
||||
private static final float FLIGHT_BLEND_PER_FRAME = 0.1f;
|
||||
private static final float CLOCK_WRAP_SECONDS = 360.0f;
|
||||
private static final long FRAME_MILLIS = 150L;
|
||||
private static final float MIN_LIGHT = 0.3f;
|
||||
|
||||
/** Per-frame knobs resolved by the module. */
|
||||
public record Style(float scale, int tintRgb, boolean depthTest) {
|
||||
}
|
||||
|
||||
private static final Map<UUID, Float> FLIGHT = new HashMap<>();
|
||||
|
||||
private WingModelRenderer() {
|
||||
}
|
||||
|
||||
public static void render(Renderer3D renderer, Player player, float tickDelta, float bodyYaw,
|
||||
WingModels.Loaded wing, Style style) {
|
||||
WingModelDef def = wing.def();
|
||||
float seconds = (player.tickCount + tickDelta) / 20.0f % CLOCK_WRAP_SECONDS;
|
||||
wing.animator().apply(wing.model(), seconds, flight(player));
|
||||
|
||||
Matrix4f anchor = anchor(player, tickDelta, bodyYaw, def, style.scale());
|
||||
MeshBuilder mesh = renderer.batchTextured(
|
||||
style.depthTest() ? LoVisualRenderPipelines.WORLD_TEXTURED_DEPTH : LoVisualRenderPipelines.WORLD_TEXTURED,
|
||||
wing.texture(), style.depthTest() ? Renderer3D.DepthMode.MAIN : Renderer3D.DepthMode.NONE);
|
||||
if (mesh == null) return;
|
||||
|
||||
float light = light(player);
|
||||
int r = Math.round((style.tintRgb() >> 16 & 0xFF) * light);
|
||||
int g = Math.round((style.tintRgb() >> 8 & 0xFF) * light);
|
||||
int b = Math.round((style.tintRgb() & 0xFF) * light);
|
||||
int frame = def.frames() > 1 ? (int) ((System.currentTimeMillis() / FRAME_MILLIS) % def.frames()) : 0;
|
||||
Sink sink = new Sink(mesh, wing.model().textureWidth(), wing.model().textureHeight(), def.frames(), frame, r, g, b);
|
||||
for (GeoBone root : wing.model().roots()) draw(root, anchor, sink);
|
||||
}
|
||||
|
||||
/** translate(player) · yaw · body (crouch) · flip · catalog offset · scale, in blocks. */
|
||||
static Matrix4f anchor(Player player, float tickDelta, float bodyYaw, WingModelDef def, float userScale) {
|
||||
Vec3 pos = player.getPosition(tickDelta);
|
||||
Matrix4f m = new Matrix4f()
|
||||
.translate((float) pos.x, (float) pos.y, (float) pos.z)
|
||||
.rotateY((float) Math.toRadians(180.0f - bodyYaw))
|
||||
.scale(-1.0f, -1.0f, 1.0f)
|
||||
.translate(0.0f, -MODEL_HEIGHT, 0.0f);
|
||||
if (player.isShiftKeyDown()) m.translate(0.0f, CROUCH_BODY_DROP, 0.0f).rotateX(CROUCH_LEAN);
|
||||
float back = player.getItemBySlot(EquipmentSlot.CHEST).isEmpty() ? 0.0f : CHEST_ARMOR_OFFSET;
|
||||
m.rotateZ((float) Math.PI)
|
||||
.translate(0.0f, def.offsetY() + ATTACHMENT_SHIFT, def.offsetZ() + back)
|
||||
.scale(def.scale() * userScale);
|
||||
return m;
|
||||
}
|
||||
|
||||
private static void draw(GeoBone bone, Matrix4f parent, Sink sink) {
|
||||
Matrix4f matrix = bone.transform(new Matrix4f(parent));
|
||||
for (GeoCube cube : bone.cubes) {
|
||||
Matrix4f cubeMatrix = cube.rotated() ? cube.transform(new Matrix4f(matrix)) : matrix;
|
||||
for (float[] face : cube.faces()) sink.quad(cubeMatrix, face);
|
||||
}
|
||||
for (GeoBone child : bone.children) draw(child, matrix, sink);
|
||||
}
|
||||
|
||||
private static float flight(Player player) {
|
||||
float target = player.isFallFlying() ? 1.0f : 0.0f;
|
||||
float current = FLIGHT.getOrDefault(player.getUUID(), target);
|
||||
current += Math.max(-FLIGHT_BLEND_PER_FRAME, Math.min(FLIGHT_BLEND_PER_FRAME, target - current));
|
||||
FLIGHT.put(player.getUUID(), current);
|
||||
return current;
|
||||
}
|
||||
|
||||
private static float light(Player player) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc.level == null) return 1.0f;
|
||||
int level = mc.level.getMaxLocalRawBrightness(BlockPos.containing(player.getEyePosition()));
|
||||
return MIN_LIGHT + (1.0f - MIN_LIGHT) * (level / 15.0f);
|
||||
}
|
||||
|
||||
/** Writes transformed quads to the mesh, mapping uv to the current texture frame. */
|
||||
private static final class Sink {
|
||||
private final MeshBuilder mesh;
|
||||
private final float texW;
|
||||
private final float texH;
|
||||
private final int frames;
|
||||
private final int frame;
|
||||
private final int r;
|
||||
private final int g;
|
||||
private final int b;
|
||||
private final Vector3f point = new Vector3f();
|
||||
|
||||
Sink(MeshBuilder mesh, int texW, int texH, int frames, int frame, int r, int g, int b) {
|
||||
this.mesh = mesh;
|
||||
this.texW = texW;
|
||||
this.texH = texH;
|
||||
this.frames = frames;
|
||||
this.frame = frame;
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
void quad(Matrix4f matrix, float[] face) {
|
||||
mesh.ensureQuadCapacity();
|
||||
int[] index = new int[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
point.set(face[i * 5] / 16.0f, face[i * 5 + 1] / 16.0f, face[i * 5 + 2] / 16.0f);
|
||||
matrix.transformPosition(point);
|
||||
float u = face[i * 5 + 3] / texW;
|
||||
float v = (face[i * 5 + 4] / texH + frame) / frames;
|
||||
index[i] = mesh.vec3(point.x, point.y, point.z).vec2(u, v).color(r, g, b, 255).next();
|
||||
}
|
||||
mesh.quad(index[0], index[1], index[2], index[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.wings.model;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.wings.model.anim.BedrockClip;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.wings.model.anim.ProceduralWings;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.wings.model.anim.WingAnimator;
|
||||
import dev.loki.lovisual.util.bedrock.geo.GeoParser;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.resources.Resource;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Lazily loads wing models from {@code assets/lovisual/wings} and keeps them until the resources reload. */
|
||||
public final class WingModels {
|
||||
private static final String NAMESPACE = "lovisual";
|
||||
|
||||
/** A model ready to pose and draw. */
|
||||
public record Loaded(WingModelDef def, GeoParser.Model model, WingAnimator animator, Identifier texture) {
|
||||
}
|
||||
|
||||
private static final Map<String, Optional<Loaded>> CACHE = new HashMap<>();
|
||||
|
||||
private WingModels() {
|
||||
}
|
||||
|
||||
public static Loaded get(WingModelDef def) {
|
||||
return CACHE.computeIfAbsent(def.name(), name -> Optional.ofNullable(load(def))).orElse(null);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
CACHE.clear();
|
||||
}
|
||||
|
||||
private static Loaded load(WingModelDef def) {
|
||||
try {
|
||||
GeoParser.Model model = GeoParser.parse(read("wings/" + def.geometry() + ".geo.json"));
|
||||
WingAnimator animator = animator(def);
|
||||
Identifier texture = Identifier.fromNamespaceAndPath(NAMESPACE, "textures/wings/" + def.texture() + ".png");
|
||||
return new Loaded(def, model, animator, texture);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
DebugLog.warn("[LoVisual] Wing model '" + def.name() + "' failed to load: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static WingAnimator animator(WingModelDef def) throws IOException {
|
||||
return switch (def.animation()) {
|
||||
case "angel" -> ProceduralWings.ANGEL;
|
||||
case "demon" -> ProceduralWings.DEMON;
|
||||
case "dragon" -> ProceduralWings.DRAGON;
|
||||
default -> BedrockClip.parse(read("wings/" + def.animation() + ".animation.json"));
|
||||
};
|
||||
}
|
||||
|
||||
private static String read(String path) throws IOException {
|
||||
Optional<Resource> resource = Minecraft.getInstance().getResourceManager()
|
||||
.getResource(Identifier.fromNamespaceAndPath(NAMESPACE, path));
|
||||
if (resource.isEmpty()) throw new IOException("missing resource " + path);
|
||||
try (BufferedReader reader = resource.get().openAsReader()) {
|
||||
return reader.lines().collect(Collectors.joining("\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.wings.model.anim;
|
||||
|
||||
import dev.loki.lovisual.util.bedrock.anim.Channel;
|
||||
import dev.loki.lovisual.util.bedrock.anim.Clip;
|
||||
import dev.loki.lovisual.util.bedrock.geo.GeoBone;
|
||||
import dev.loki.lovisual.util.bedrock.geo.GeoParser;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/** The first clip of a Bedrock {@code .animation.json}, applied to the bones of a wing model. */
|
||||
public final class BedrockClip implements WingAnimator {
|
||||
private final Clip clip;
|
||||
|
||||
private BedrockClip(Clip clip) {
|
||||
this.clip = clip;
|
||||
}
|
||||
|
||||
public float length() {
|
||||
return clip.length();
|
||||
}
|
||||
|
||||
public static BedrockClip parse(String json) {
|
||||
Map<String, Clip> clips = Clip.parseAll(json);
|
||||
return new BedrockClip(clips.isEmpty() ? null : clips.values().iterator().next());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(GeoParser.Model model, float seconds, float flight) {
|
||||
model.resetPose();
|
||||
if (clip == null) return;
|
||||
float length = Math.max(clip.length(), 0.001f);
|
||||
float time = clip.loops() ? seconds % length : Math.min(seconds, length);
|
||||
float[] v = new float[3];
|
||||
for (Map.Entry<String, Clip.Tracks> entry : clip.bones().entrySet()) {
|
||||
GeoBone bone = model.bones().get(entry.getKey());
|
||||
if (bone == null) continue;
|
||||
Clip.Tracks tracks = entry.getValue();
|
||||
Channel rotation = tracks.rotation();
|
||||
if (rotation != null) {
|
||||
rotation.sample(time, v, 0);
|
||||
bone.rot[0] = bone.baseRot[0] + (float) Math.toRadians(-v[0]);
|
||||
bone.rot[1] = bone.baseRot[1] + (float) Math.toRadians(-v[1]);
|
||||
bone.rot[2] = bone.baseRot[2] + (float) Math.toRadians(v[2]);
|
||||
}
|
||||
Channel position = tracks.position();
|
||||
if (position != null) {
|
||||
position.sample(time, v, 0);
|
||||
bone.offset[0] = -v[0];
|
||||
bone.offset[1] = v[1];
|
||||
bone.offset[2] = v[2];
|
||||
}
|
||||
Channel scale = tracks.scale();
|
||||
if (scale != null) {
|
||||
scale.sample(time, bone.scale, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.wings.model.anim;
|
||||
|
||||
import dev.loki.lovisual.util.bedrock.geo.GeoBone;
|
||||
import dev.loki.lovisual.util.bedrock.geo.GeoParser;
|
||||
|
||||
/**
|
||||
* Code-driven flaps for the three models that ship without an animation file. Angles are degrees added to the
|
||||
* pose stored in the model, positions are pixels, and the clock runs in degrees (seconds x speed).
|
||||
*/
|
||||
public final class ProceduralWings {
|
||||
private static final float ROOT_SCALE = 0.6f;
|
||||
private static final float DEGREES_PER_SECOND = 200.0f;
|
||||
|
||||
private ProceduralWings() {
|
||||
}
|
||||
|
||||
public static final WingAnimator ANGEL = (model, seconds, flight) -> {
|
||||
model.resetPose();
|
||||
float t = seconds * DEGREES_PER_SECOND;
|
||||
scaleRoots(model, ROOT_SCALE);
|
||||
float flap = cos(10 + t);
|
||||
float sway = sin(10 + t);
|
||||
pose(model, "Left_Wing", -18.2468f + flap * 10, -36.3088f + sway * 15, 18.01613f, -0.0288f, 0.20524f, 1.18197f);
|
||||
pose(model, "Right_Wing", -18.2468f + flap * 10, 36.3088f - sway * 15, -18.01609f, -0.0288f, 0.2052f, 1.18197f);
|
||||
pose(model, "bone6", 1.1552f - flap * 10, -0.543f - cos(t) * 25, 3.16854f, -0.90088f, 0.04987f, -0.43119f);
|
||||
pose(model, "bone4", 1.1552f - flap * 10, 0.543f + cos(t) * 25, -3.1685f, 0.9009f, 0.04987f, -0.43119f);
|
||||
pose(model, "bone7", flap * 5, -sway * 15, -sway * 15);
|
||||
pose(model, "bone5", flap * 5, sway * 15, sway * 15);
|
||||
pose(model, "bone3", -0.0082f + cos(t) * 10, 12.49978f, -7.57475f, -0.87628f, 1.75491f, 0.39042f);
|
||||
pose(model, "bone8", -0.0082f + cos(t) * 10, -12.4998f, 7.5748f, 0.8763f, 1.75491f, 0.39042f);
|
||||
pose(model, "bone2", 6.4724f - cos(60 + t) * 5, 5.1344f + sin(60 + t) * 5, -12.5338f);
|
||||
pose(model, "bone9", 6.4724f - cos(60 + t) * 5, -5.1344f - sin(60 + t) * 5, 12.5338f);
|
||||
pose(model, "bone10", 6.4876f + cos(200 + t) * 5, -0.97f + sin(300 + t) * 5, -6.9044f - sin(220 + t) * 7,
|
||||
-0.89933f, -0.02767f, -0.01524f);
|
||||
pose(model, "bone11", 6.4876f + cos(200 + t) * 5, 0.97f - sin(300 + t) * 5, 6.9044f + sin(220 + t) * 7,
|
||||
0.8993f, -0.0277f, -0.0152f);
|
||||
};
|
||||
|
||||
public static final WingAnimator DEMON = (model, seconds, flight) -> {
|
||||
model.resetPose();
|
||||
float t = seconds * DEGREES_PER_SECOND;
|
||||
scaleRoots(model, ROOT_SCALE);
|
||||
pose(model, "Wing3", cos(t) * 15 + 10, 45 - cos(t) * 15, 0);
|
||||
pose(model, "Wing2", cos(t) * 15 + 10, -45 + cos(t) * 15, 0);
|
||||
pose(model, "bone8", 0, -cos(50 - t) * 20, 0);
|
||||
pose(model, "bone2", 0, cos(50 - t) * 20, 0);
|
||||
pose(model, "bone9", 0, -cos(80 - t) * 25, 0);
|
||||
pose(model, "bone3", 0, cos(80 - t) * 25, 0);
|
||||
};
|
||||
|
||||
/** Standing and gliding flaps, blended by {@code flight}; the wings flap faster and the model grows when gliding. */
|
||||
public static final WingAnimator DRAGON = (model, seconds, flight) -> {
|
||||
model.resetPose();
|
||||
float f = Math.max(0.0f, Math.min(flight, 1.0f));
|
||||
float slow = seconds * 300.0f;
|
||||
float fast = seconds * 600.0f;
|
||||
scaleRoots(model, mix(f, 0.9f, 1.0f));
|
||||
|
||||
float leftYaw = mix(f, -30 + cos(slow) * 20, -27.5839f + cos(fast) * 20);
|
||||
float rightYaw = mix(f, 30 - cos(slow) * 20, 27.5839f - cos(fast) * 20);
|
||||
float pitch = mix(f, cos(-60 + slow) * 10, 7.8279f + cos(-60 + fast) * 10);
|
||||
float roll = mix(f, 0, -12.46225f);
|
||||
pose(model, "left_wing", pitch, leftYaw, roll);
|
||||
pose(model, "right_wing", pitch, rightYaw, -roll);
|
||||
float tip = mix(f, cos(90 + slow), cos(90 + fast)) * 20;
|
||||
pose(model, "left_tip", 0, -tip, 0);
|
||||
pose(model, "right_tip", 0, tip, 0);
|
||||
|
||||
float stretch = mix(f, 1.2f + cos(90 + slow) * 0.2f, 1.2f + cos(90 + fast) * 0.2f);
|
||||
stretch(model, "left_wing", stretch);
|
||||
stretch(model, "right_wing", stretch);
|
||||
};
|
||||
|
||||
private static void pose(GeoParser.Model model, String name, float rx, float ry, float rz) {
|
||||
GeoBone bone = model.bones().get(name);
|
||||
if (bone == null) return;
|
||||
bone.rot[0] = bone.baseRot[0] + (float) Math.toRadians(-rx);
|
||||
bone.rot[1] = bone.baseRot[1] + (float) Math.toRadians(-ry);
|
||||
bone.rot[2] = bone.baseRot[2] + (float) Math.toRadians(rz);
|
||||
}
|
||||
|
||||
private static void pose(GeoParser.Model model, String name, float rx, float ry, float rz, float ox, float oy, float oz) {
|
||||
pose(model, name, rx, ry, rz);
|
||||
GeoBone bone = model.bones().get(name);
|
||||
if (bone == null) return;
|
||||
bone.offset[0] = -ox;
|
||||
bone.offset[1] = oy;
|
||||
bone.offset[2] = oz;
|
||||
}
|
||||
|
||||
private static void stretch(GeoParser.Model model, String name, float widthScale) {
|
||||
GeoBone bone = model.bones().get(name);
|
||||
if (bone == null) return;
|
||||
bone.scale[0] = widthScale;
|
||||
bone.scale[2] = 1.1f;
|
||||
}
|
||||
|
||||
private static void scaleRoots(GeoParser.Model model, float factor) {
|
||||
for (GeoBone root : model.roots()) {
|
||||
root.scale[0] = factor;
|
||||
root.scale[1] = factor;
|
||||
root.scale[2] = factor;
|
||||
}
|
||||
}
|
||||
|
||||
private static float mix(float t, float a, float b) {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
|
||||
static float cos(float degrees) {
|
||||
return (float) Math.cos(Math.toRadians(degrees));
|
||||
}
|
||||
|
||||
static float sin(float degrees) {
|
||||
return (float) Math.sin(Math.toRadians(degrees));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.wings.model.anim;
|
||||
|
||||
import dev.loki.lovisual.util.bedrock.geo.GeoParser;
|
||||
|
||||
/** Poses a wing model for a moment in time. */
|
||||
public interface WingAnimator {
|
||||
/**
|
||||
* @param seconds animation clock in seconds
|
||||
* @param flight 0 while standing, 1 while gliding; models without a flight pose ignore it
|
||||
*/
|
||||
void apply(GeoParser.Model model, float seconds, float flight);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package dev.loki.lovisual.features.theme;
|
|||
import dev.loki.lovisual.config.definition.SettingDef;
|
||||
import dev.loki.lovisual.config.definition.SettingOwner;
|
||||
import dev.loki.lovisual.config.profile.service.ConfigProfileStorage;
|
||||
import dev.loki.lovisual.config.values.mode.ModeValue;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||
|
|
@ -39,6 +40,10 @@ public final class EditableClickGuiTheme implements SettingOwner {
|
|||
private final GradientValues surfaceGradient;
|
||||
private final GradientValues cardGradient;
|
||||
private final GradientValues strokeGradient;
|
||||
private final ModeValue backdropMode;
|
||||
private final NumberValue<Double> backdropIntensity;
|
||||
private final NumberValue<Double> backdropSpeed;
|
||||
private final NumberValue<Double> backdropScale;
|
||||
|
||||
private EditableClickGuiTheme(String id, Themes.ThemeEntry entry) {
|
||||
Themes.ThemeEntry src = entry != null ? entry : Theme.currentEntry();
|
||||
|
|
@ -63,6 +68,11 @@ public final class EditableClickGuiTheme implements SettingOwner {
|
|||
surfaceGradient = new GradientValues("surface", src.surfaceGradient(), theme.surface());
|
||||
cardGradient = new GradientValues("card", src.cardGradient(), theme.cardEnabled());
|
||||
strokeGradient = new GradientValues("stroke", src.strokeGradient(), theme.windowStroke());
|
||||
Backdrop backdrop = src.backdrop();
|
||||
backdropMode = new ModeValue("backdrop_mode", backdrop.mode().id(), backdropModeIds());
|
||||
backdropIntensity = new NumberValue<>("backdrop_intensity", (double) backdrop.intensity(), 0.0, (double) Backdrop.MAX_INTENSITY);
|
||||
backdropSpeed = new NumberValue<>("backdrop_speed", (double) backdrop.speed(), (double) Backdrop.MIN_SPEED, (double) Backdrop.MAX_SPEED);
|
||||
backdropScale = new NumberValue<>("backdrop_scale", (double) backdrop.scale(), (double) Backdrop.MIN_SCALE, (double) Backdrop.MAX_SCALE);
|
||||
}
|
||||
|
||||
public static EditableClickGuiTheme existing(String id) {
|
||||
|
|
@ -106,6 +116,10 @@ public final class EditableClickGuiTheme implements SettingOwner {
|
|||
surfaceGradient.addSettings(out, this);
|
||||
cardGradient.addSettings(out, this);
|
||||
strokeGradient.addSettings(out, this);
|
||||
add(out, SettingDef.mode("backdrop_mode", backdropMode));
|
||||
add(out, SettingDef.number("backdrop_intensity", backdropIntensity).visibleWhen(this::hasBackdrop));
|
||||
add(out, SettingDef.number("backdrop_speed", backdropSpeed).visibleWhen(this::hasBackdrop));
|
||||
add(out, SettingDef.number("backdrop_scale", backdropScale).visibleWhen(this::hasBackdrop));
|
||||
for (Setting setting : out) {
|
||||
setting.setParent(this);
|
||||
setting.preflightI18n();
|
||||
|
|
@ -125,7 +139,20 @@ public final class EditableClickGuiTheme implements SettingOwner {
|
|||
return new Themes.ThemeEntry(id, title(), false, theme,
|
||||
windowGradient.toSpec(theme.windowBg()), headerGradient.toSpec(theme.windowHeader()),
|
||||
surfaceGradient.toSpec(theme.surface()), cardGradient.toSpec(theme.cardEnabled()),
|
||||
strokeGradient.toSpec(theme.windowStroke()));
|
||||
strokeGradient.toSpec(theme.windowStroke()), backdrop());
|
||||
}
|
||||
|
||||
private boolean hasBackdrop() {
|
||||
return Backdrop.Mode.byId(backdropMode.get()) != Backdrop.Mode.NONE;
|
||||
}
|
||||
|
||||
private Backdrop backdrop() {
|
||||
return new Backdrop(Backdrop.Mode.byId(backdropMode.get()), backdropIntensity.get().floatValue(),
|
||||
backdropSpeed.get().floatValue(), backdropScale.get().floatValue());
|
||||
}
|
||||
|
||||
private static String[] backdropModeIds() {
|
||||
return java.util.Arrays.stream(Backdrop.Mode.values()).map(Backdrop.Mode::id).toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ public final class ThemeStore {
|
|||
ThemeEntry normalized = new ThemeEntry(id,
|
||||
entry.name() == null || entry.name().isBlank() ? id : entry.name().trim(),
|
||||
false, entry.theme(), entry.windowGradient(), entry.headerGradient(),
|
||||
entry.surfaceGradient(), entry.cardGradient(), entry.strokeGradient());
|
||||
entry.surfaceGradient(), entry.cardGradient(), entry.strokeGradient(), entry.backdrop());
|
||||
boolean replaced = false;
|
||||
for (int i = 0; i < customThemes.size(); i++) {
|
||||
if (customThemes.get(i).getId().equals(id)) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package dev.loki.lovisual.features.theme;
|
||||
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -82,6 +83,14 @@ public enum Themes {
|
|||
static boolean isCustomTheme(String id) { return STORE.isCustomId(id); }
|
||||
static String uniqueCustomId(String name) { return STORE.uniqueCustomId(name); }
|
||||
static Theme classic() { return ThemePresets1.CLASSIC; }
|
||||
/** What the ClickGUI backdrop draws this frame: the target, the backdrop it replaces and the wipe progress (1 = done). */
|
||||
public record BackdropView(Backdrop target, Backdrop outgoing, float wipe) { }
|
||||
|
||||
public static BackdropView backdropView() {
|
||||
ThemesTransition.BackdropFrame frame = ThemesTransition.backdropFrame();
|
||||
return new BackdropView(frame.target(), frame.outgoing(), frame.progress());
|
||||
}
|
||||
|
||||
static void setTheme(String id) { ThemesTransition.setTheme(id); }
|
||||
static void setTheme(Theme t) { ThemesTransition.setTheme(t); }
|
||||
static GradientSpec flatGradient(int color) { return ThemesBlending.flatGradient(color); }
|
||||
|
|
@ -119,7 +128,8 @@ public enum Themes {
|
|||
String displayName = name == null || name.isBlank() ? "Custom Theme" : name.trim();
|
||||
String id = STORE.uniqueCustomId(displayName);
|
||||
ThemeEntry entry = new ThemeEntry(id, displayName, false, src.theme(),
|
||||
src.windowGradient(), src.headerGradient(), src.surfaceGradient(), src.cardGradient(), src.strokeGradient());
|
||||
src.windowGradient(), src.headerGradient(), src.surfaceGradient(), src.cardGradient(), src.strokeGradient(),
|
||||
src.backdrop());
|
||||
return saveCustomTheme(entry, select);
|
||||
}
|
||||
|
||||
|
|
@ -161,8 +171,26 @@ public enum Themes {
|
|||
public record ThemeEntry(
|
||||
String id, String name, boolean builtin, Theme theme,
|
||||
GradientSpec windowGradient, GradientSpec headerGradient,
|
||||
GradientSpec surfaceGradient, GradientSpec cardGradient, GradientSpec strokeGradient
|
||||
GradientSpec surfaceGradient, GradientSpec cardGradient, GradientSpec strokeGradient,
|
||||
Backdrop backdrop
|
||||
) {
|
||||
public ThemeEntry {
|
||||
backdrop = backdrop == null ? Backdrop.NONE : backdrop;
|
||||
}
|
||||
|
||||
/** An entry without a shader backdrop. */
|
||||
public ThemeEntry(String id, String name, boolean builtin, Theme theme,
|
||||
GradientSpec windowGradient, GradientSpec headerGradient,
|
||||
GradientSpec surfaceGradient, GradientSpec cardGradient, GradientSpec strokeGradient) {
|
||||
this(id, name, builtin, theme, windowGradient, headerGradient, surfaceGradient, cardGradient,
|
||||
strokeGradient, Backdrop.NONE);
|
||||
}
|
||||
|
||||
public ThemeEntry withBackdrop(Backdrop value) {
|
||||
return new ThemeEntry(id, name, builtin, theme, windowGradient, headerGradient, surfaceGradient,
|
||||
cardGradient, strokeGradient, value);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package dev.loki.lovisual.features.theme;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The animated shader backdrop of a theme, drawn behind the ClickGUI window and tinted with the theme's accents.
|
||||
* {@code intensity} 0 means off; {@code speed} and {@code scale} stretch time and the pattern.
|
||||
*/
|
||||
public record Backdrop(Mode mode, float intensity, float speed, float scale) {
|
||||
public enum Mode {
|
||||
NONE, IRIDESCENCE, MAGMA, DIGITAL_RAIN, GRID_HORIZON, SINGULARITY, EMBER;
|
||||
|
||||
public String id() {
|
||||
return name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
/** Mode number the shader switches on; 0 for none. */
|
||||
public int shaderIndex() {
|
||||
return ordinal();
|
||||
}
|
||||
|
||||
public static Mode byId(String id) {
|
||||
if (id != null) {
|
||||
for (Mode mode : values()) {
|
||||
if (mode.id().equalsIgnoreCase(id.trim())) return mode;
|
||||
}
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
}
|
||||
|
||||
public static final float MAX_INTENSITY = 1.5f;
|
||||
public static final float MIN_SPEED = 0.1f;
|
||||
public static final float MAX_SPEED = 3.0f;
|
||||
public static final float MIN_SCALE = 0.5f;
|
||||
public static final float MAX_SCALE = 2.5f;
|
||||
public static final Backdrop NONE = new Backdrop(Mode.NONE, 0.0f, 1.0f, 1.0f);
|
||||
|
||||
public Backdrop {
|
||||
mode = mode == null ? Mode.NONE : mode;
|
||||
intensity = clamp(intensity, 0.0f, MAX_INTENSITY);
|
||||
speed = clamp(speed, MIN_SPEED, MAX_SPEED);
|
||||
scale = clamp(scale, MIN_SCALE, MAX_SCALE);
|
||||
}
|
||||
|
||||
public static Backdrop of(Mode mode, float intensity) {
|
||||
return new Backdrop(mode, intensity, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return mode != Mode.NONE && intensity > 0.0f;
|
||||
}
|
||||
|
||||
public Backdrop withIntensity(float value) {
|
||||
return new Backdrop(mode, value, speed, scale);
|
||||
}
|
||||
|
||||
/** Same mode: all numbers blend. Different mode: the new backdrop fades in from nothing. */
|
||||
public Backdrop lerp(Backdrop to, float t) {
|
||||
float f = clamp(t, 0.0f, 1.0f);
|
||||
if (mode != to.mode) return new Backdrop(to.mode, to.intensity * f, to.speed, to.scale);
|
||||
return new Backdrop(mode, intensity + (to.intensity - intensity) * f,
|
||||
speed + (to.speed - speed) * f, scale + (to.scale - scale) * f);
|
||||
}
|
||||
|
||||
private static float clamp(float value, float min, float max) {
|
||||
if (Float.isNaN(value)) return min;
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
}
|
||||
|
|
@ -100,7 +100,8 @@ final class ThemesBlending {
|
|||
blendGradient(from.headerGradient(), to.headerGradient(), t),
|
||||
blendGradient(from.surfaceGradient(), to.surfaceGradient(), t),
|
||||
blendGradient(from.cardGradient(), to.cardGradient(), t),
|
||||
blendGradient(from.strokeGradient(), to.strokeGradient(), t)
|
||||
blendGradient(from.strokeGradient(), to.strokeGradient(), t),
|
||||
from.backdrop().lerp(to.backdrop(), t)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ final class ThemesProfileCodec {
|
|||
writeGradient(out, "surface", entry.surfaceGradient());
|
||||
writeGradient(out, "card", entry.cardGradient());
|
||||
writeGradient(out, "stroke", entry.strokeGradient());
|
||||
out.put("backdropMode", entry.backdrop().mode().id());
|
||||
out.put("backdropIntensity", entry.backdrop().intensity());
|
||||
out.put("backdropSpeed", entry.backdrop().speed());
|
||||
out.put("backdropScale", entry.backdrop().scale());
|
||||
return out;
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +78,11 @@ final class ThemesProfileCodec {
|
|||
readGradient(map, "header", baseEntry.headerGradient(), theme.windowHeader()),
|
||||
readGradient(map, "surface", baseEntry.surfaceGradient(), theme.surface()),
|
||||
readGradient(map, "card", baseEntry.cardGradient(), theme.cardEnabled()),
|
||||
readGradient(map, "stroke", baseEntry.strokeGradient(), theme.windowStroke())
|
||||
readGradient(map, "stroke", baseEntry.strokeGradient(), theme.windowStroke()),
|
||||
new Backdrop(Backdrop.Mode.byId(stringValue(map.get("backdropMode"), baseEntry.backdrop().mode().id())),
|
||||
floatValue(map.get("backdropIntensity"), baseEntry.backdrop().intensity()),
|
||||
floatValue(map.get("backdropSpeed"), baseEntry.backdrop().speed()),
|
||||
floatValue(map.get("backdropScale"), baseEntry.backdrop().scale()))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,25 @@ final class ThemesTransition {
|
|||
/** Total cross-fade duration in milliseconds; matches {@code Themes.THEME_TRANSITION_MS}. */
|
||||
ThemesTransition() { }
|
||||
|
||||
/** Length of the shader wipe that replaces the old backdrop with the new one. */
|
||||
static final long WIPE_MS = 700L;
|
||||
private static long wipeStartMs = -WIPE_MS;
|
||||
private static Backdrop wipeFrom = Backdrop.NONE;
|
||||
|
||||
/** Which backdrops to draw now: the target, the one it replaces, and the wipe progress (1 when finished). */
|
||||
record BackdropFrame(Backdrop target, Backdrop outgoing, float progress) {
|
||||
boolean wiping() {
|
||||
return progress < 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
static BackdropFrame backdropFrame() {
|
||||
Backdrop target = Themes.currentEntry.backdrop();
|
||||
long elapsed = Util.getMillis() - wipeStartMs;
|
||||
if (elapsed >= WIPE_MS || elapsed < 0L) return new BackdropFrame(target, Backdrop.NONE, 1.0f);
|
||||
return new BackdropFrame(target, wipeFrom, AnimationUtility.easeOutCubic((float) elapsed / (float) WIPE_MS));
|
||||
}
|
||||
|
||||
/** Current theme with any in-flight transition applied; safe to call every frame. */
|
||||
static Theme theme() {
|
||||
return animatedTheme();
|
||||
|
|
@ -104,6 +123,8 @@ final class ThemesTransition {
|
|||
Themes.transitionFromTheme = fromTheme != null ? fromTheme : Themes.current;
|
||||
Themes.transitionFromEntry = fromEntry != null ? fromEntry : Themes.currentEntry;
|
||||
Themes.transitionStartMs = Util.getMillis();
|
||||
wipeFrom = Themes.currentEntry.backdrop();
|
||||
wipeStartMs = Themes.transitionStartMs;
|
||||
Themes.transitionActive = true;
|
||||
Themes.animatedCacheKey = -1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,8 @@ static final ThemeEntry AMBER_ENTRY = new ThemeEntry(
|
|||
Themes.flatGradient(AMBER.surface()),
|
||||
Themes.flatGradient(AMBER.cardEnabled()),
|
||||
Themes.flatGradient(AMBER.windowStroke())
|
||||
);
|
||||
)
|
||||
.withBackdrop(Backdrop.of(Backdrop.Mode.EMBER, 0.7f));
|
||||
static final ThemeEntry NOIR_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_NOIR,
|
||||
"Noir",
|
||||
|
|
|
|||
|
|
@ -115,7 +115,8 @@ static final ThemeEntry NITRO_ENTRY = new ThemeEntry(
|
|||
new GradientSpec(true, 0xFF1A1F36, 0xFF121728, 90f),
|
||||
new GradientSpec(true, 0xFF5A37D5, 0xFF2AD1FF, 45f),
|
||||
new GradientSpec(true, 0xFF6C4BFF, 0xFF4BD0FF, 45f)
|
||||
);
|
||||
)
|
||||
.withBackdrop(Backdrop.of(Backdrop.Mode.SINGULARITY, 0.8f));
|
||||
static final ThemeEntry SUNSET_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_SUNSET,
|
||||
"Sunset",
|
||||
|
|
|
|||
|
|
@ -126,7 +126,8 @@ static final ThemeEntry DUSK_ENTRY = new ThemeEntry(
|
|||
new GradientSpec(true, 0xFF1D1626, 0xFF141018, 90f),
|
||||
new GradientSpec(true, 0xFF7C4BFF, 0xFFFF9B3D, 45f),
|
||||
new GradientSpec(true, 0xFFA775FF, 0xFFFF7A3A, 45f)
|
||||
);
|
||||
)
|
||||
.withBackdrop(Backdrop.of(Backdrop.Mode.GRID_HORIZON, 0.8f));
|
||||
static final ThemeEntry PRISM_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_PRISM,
|
||||
"Prism",
|
||||
|
|
@ -137,7 +138,8 @@ static final ThemeEntry PRISM_ENTRY = new ThemeEntry(
|
|||
new GradientSpec(true, 0xFF151C2E, 0xFF101522, 90f),
|
||||
new GradientSpec(true, 0xFF59C4FF, 0xFFB35CFF, 45f),
|
||||
new GradientSpec(true, 0xFF7AD4FF, 0xFFE36BFF, 45f)
|
||||
);
|
||||
)
|
||||
.withBackdrop(Backdrop.of(Backdrop.Mode.IRIDESCENCE, 0.7f));
|
||||
static final ThemeEntry CINDER_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_CINDER,
|
||||
"Cinder",
|
||||
|
|
@ -148,7 +150,8 @@ static final ThemeEntry CINDER_ENTRY = new ThemeEntry(
|
|||
new GradientSpec(true, 0xFF1A1214, 0xFF120E0F, 90f),
|
||||
new GradientSpec(true, 0xFFDC4A4A, 0xFF401010, 45f),
|
||||
new GradientSpec(true, 0xFFE26A6A, 0xFF7A2A2A, 45f)
|
||||
);
|
||||
)
|
||||
.withBackdrop(Backdrop.of(Backdrop.Mode.MAGMA, 0.8f));
|
||||
static final ThemeEntry FOREST_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_FOREST,
|
||||
"Forest",
|
||||
|
|
@ -159,5 +162,6 @@ static final ThemeEntry FOREST_ENTRY = new ThemeEntry(
|
|||
new GradientSpec(true, 0xFF161F18, 0xFF101611, 90f),
|
||||
new GradientSpec(true, 0xFF7DD36B, 0xFFC6E886, 45f),
|
||||
new GradientSpec(true, 0xFF9BE07C, 0xFF6BC26A, 45f)
|
||||
);
|
||||
)
|
||||
.withBackdrop(Backdrop.of(Backdrop.Mode.DIGITAL_RAIN, 0.7f));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ final class ThemeStoreJson {
|
|||
gradients.add("card", gradientToJson(entry.cardGradient()));
|
||||
gradients.add("stroke", gradientToJson(entry.strokeGradient()));
|
||||
obj.add("gradients", gradients);
|
||||
obj.add("backdrop", backdropToJson(entry.backdrop()));
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +180,24 @@ final class ThemeStoreJson {
|
|||
parseGradient(getObject(gradients, "header"), preset != null ? preset.headerGradient() : new GradientSpec(false, windowHeader, windowHeader, 90f), windowHeader),
|
||||
parseGradient(getObject(gradients, "surface"), preset != null ? preset.surfaceGradient() : new GradientSpec(false, surface, surface, 90f), surface),
|
||||
parseGradient(getObject(gradients, "card"), preset != null ? preset.cardGradient() : new GradientSpec(false, cardEnabled, cardEnabled, 90f), cardEnabled),
|
||||
parseGradient(getObject(gradients, "stroke"), preset != null ? preset.strokeGradient() : new GradientSpec(false, windowStroke, windowStroke, 90f), windowStroke));
|
||||
parseGradient(getObject(gradients, "stroke"), preset != null ? preset.strokeGradient() : new GradientSpec(false, windowStroke, windowStroke, 90f), windowStroke),
|
||||
parseBackdrop(getObject(obj, "backdrop"), preset != null ? preset.backdrop() : Backdrop.NONE));
|
||||
}
|
||||
|
||||
static JsonObject backdropToJson(Backdrop backdrop) {
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("mode", backdrop.mode().id());
|
||||
json.addProperty("intensity", backdrop.intensity());
|
||||
json.addProperty("speed", backdrop.speed());
|
||||
json.addProperty("scale", backdrop.scale());
|
||||
return json;
|
||||
}
|
||||
|
||||
static Backdrop parseBackdrop(JsonObject map, Backdrop fallback) {
|
||||
if (map == null || map.size() == 0) return fallback;
|
||||
return new Backdrop(Backdrop.Mode.byId(getString(map, "mode", fallback.mode().id())),
|
||||
getFloat(map, "intensity", fallback.intensity()), getFloat(map, "speed", fallback.speed()),
|
||||
getFloat(map, "scale", fallback.scale()));
|
||||
}
|
||||
|
||||
static GradientSpec parseGradient(JsonObject map, GradientSpec fallback, int baseColor) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
package dev.loki.lovisual.mixins.render.glint;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.glint.EnchantGlint;
|
||||
import net.minecraft.client.renderer.rendertype.TextureTransform;
|
||||
import net.minecraft.client.renderer.state.OptionsRenderState;
|
||||
import org.joml.Matrix4f;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
/** EnchantGlint: скорость прокрутки и масштаб полос блеска в матрице текстуры. */
|
||||
/** EnchantGlint: скорость прокрутки, масштаб полос и своя анимация стилей в матрице текстуры. */
|
||||
@Mixin(TextureTransform.class)
|
||||
public abstract class GlintTextureMixin {
|
||||
|
||||
|
|
@ -25,4 +28,11 @@ public abstract class GlintTextureMixin {
|
|||
EnchantGlint glint = Modules.get(EnchantGlint.class);
|
||||
return glint == null ? state.glintSpeed : state.glintSpeed * glint.speedFactor();
|
||||
}
|
||||
|
||||
@ModifyReturnValue(method = "setupGlintTexturing", at = @At("RETURN"))
|
||||
private static Matrix4f lovisual$styleMotion(Matrix4f vanilla, @Local(argsOnly = true) float scale) {
|
||||
EnchantGlint glint = Modules.get(EnchantGlint.class);
|
||||
Matrix4f custom = glint == null ? null : glint.motion(scale);
|
||||
return custom == null ? vanilla : custom;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package dev.loki.lovisual.mixins.render.godrays;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.godrays.GodRaysSun;
|
||||
import dev.loki.lovisual.mixins.accessors.world.LevelRendererAccessor;
|
||||
import net.minecraft.client.renderer.LevelRenderer;
|
||||
import net.minecraft.client.renderer.state.level.SkyRenderState;
|
||||
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;
|
||||
|
||||
/** Records the sun angle and sunrise glow of the frame for the God Rays pass. */
|
||||
@Mixin(LevelRenderer.class)
|
||||
public abstract class GodRaysSkyMixin {
|
||||
@Inject(method = "addSkyPass", at = @At("HEAD"))
|
||||
private void lovisual$captureSun(CallbackInfo ci) {
|
||||
SkyRenderState sky = ((LevelRendererAccessor) this).lovisual$getWorldRenderState().skyRenderState;
|
||||
if (sky == null) {
|
||||
GodRaysSun.invalidate();
|
||||
} else {
|
||||
GodRaysSun.capture(sky.sunAngle, sky.sunriseAndSunsetColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package dev.loki.lovisual.mixins.render.knife;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.Knife;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.entity.layers.PlayerItemInHandLayer;
|
||||
import net.minecraft.client.renderer.entity.player.AvatarRenderer;
|
||||
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
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;
|
||||
|
||||
/** Knife: draws the skin in your own third person hand instead of the sword. */
|
||||
@Mixin(PlayerItemInHandLayer.class)
|
||||
public abstract class KnifeHandLayerMixin {
|
||||
private static final float HAND_PITCH_DEGREES = -90.0f;
|
||||
private static final float HAND_YAW_DEGREES = 180.0f;
|
||||
|
||||
@Inject(method = "submitArmWithItem(Lnet/minecraft/client/renderer/entity/state/AvatarRenderState;Lnet/minecraft/client/renderer/item/ItemStackRenderState;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/entity/HumanoidArm;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;I)V",
|
||||
at = @At("HEAD"), cancellable = true)
|
||||
private void lovisual$knifeThirdPerson(AvatarRenderState state, ItemStackRenderState itemState, ItemStack item,
|
||||
HumanoidArm arm, PoseStack matrices, SubmitNodeCollector queue, int light,
|
||||
CallbackInfo ci) {
|
||||
Knife knife = Modules.get(Knife.class);
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
LocalPlayer player = mc.player;
|
||||
if (knife == null || player == null || player.getId() != state.id || !Knife.isKnife(item)) return;
|
||||
AvatarRenderer<?> renderer = mc.getEntityRenderDispatcher().getPlayerRenderer(player);
|
||||
if (renderer == null) return;
|
||||
matrices.pushPose();
|
||||
renderer.getModel().translateToHand(state, arm, matrices);
|
||||
matrices.mulPose(Axis.XP.rotationDegrees(HAND_PITCH_DEGREES));
|
||||
matrices.mulPose(Axis.YP.rotationDegrees(HAND_YAW_DEGREES));
|
||||
boolean drawn = knife.renderThirdPerson(item, arm, matrices, queue, light);
|
||||
matrices.popPose();
|
||||
if (drawn) ci.cancel();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package dev.loki.lovisual.mixins.render.knife;
|
||||
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.Knife;
|
||||
import net.minecraft.client.Minecraft;
|
||||
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;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** Knife: left click plays the attack clips, right click the third one. */
|
||||
@Mixin(Minecraft.class)
|
||||
public abstract class KnifeInputMixin {
|
||||
|
||||
@Inject(method = "startAttack", at = @At("HEAD"))
|
||||
private void lovisual$knifePrimary(CallbackInfoReturnable<Boolean> cir) {
|
||||
Knife knife = Modules.get(Knife.class);
|
||||
if (knife != null) knife.onAttack(true);
|
||||
}
|
||||
|
||||
@Inject(method = "startUseItem", at = @At("RETURN"))
|
||||
private void lovisual$knifeSecondary(CallbackInfo ci) {
|
||||
Knife knife = Modules.get(Knife.class);
|
||||
if (knife != null) knife.onAttack(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package dev.loki.lovisual.mixins.render.knife;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.knife.Knife;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.client.renderer.ItemInHandRenderer;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Knife: replaces the first person sword with the skin's view model. Runs before the other hand hooks
|
||||
* (low priority) so cancelling here cannot leave their pushed poses unpopped.
|
||||
*/
|
||||
@Mixin(value = ItemInHandRenderer.class, priority = 500)
|
||||
public abstract class KnifeItemInHandMixin {
|
||||
|
||||
@Inject(method = "submitArmWithItem", at = @At("HEAD"), cancellable = true)
|
||||
private void lovisual$knifeFirstPerson(AbstractClientPlayer player, float tickDelta, float pitch,
|
||||
InteractionHand hand, float swingProgress, ItemStack item, float equipProgress,
|
||||
PoseStack matrices, SubmitNodeCollector queue, int light, CallbackInfo ci) {
|
||||
Knife knife = Modules.get(Knife.class);
|
||||
if (knife == null || player.isScoping()) return;
|
||||
if (knife.renderFirstPerson(player, hand, item, matrices, queue, light, equipProgress)) ci.cancel();
|
||||
}
|
||||
}
|
||||
|
|
@ -141,6 +141,8 @@ public enum LoVisualRenderPipelines {
|
|||
public static final RenderPipeline POST_FX = add(ScreenPipelines.screen("post_fx", SHADER_POST_FX_FRAG, "PostFX", "u_Texture", "u_Lut"));
|
||||
/** Fullscreen camera-only perceptual motion blur (pos2). */
|
||||
public static final RenderPipeline MOTION_BLUR = add(ScreenPipelines.screen("motion_blur", SHADER_MOTION_BLUR_FRAG, "MotionBlur", "u_Texture", "u_PreviousColor"));
|
||||
/** Fullscreen sun shafts: radial sky blur towards the sun, added to the scene (pos2). */
|
||||
public static final RenderPipeline GOD_RAYS = add(ScreenPipelines.screen("godrays", SHADER_GOD_RAYS_FRAG, "GodRays", "u_Texture", "u_MainDepth"));
|
||||
/** Fullscreen scene-depth-aware far depth of field (pos2). */
|
||||
public static final RenderPipeline DEPTH_OF_FIELD = add(ScreenPipelines.screen("depth_of_field", SHADER_DEPTH_OF_FIELD_FRAG, "DepthOfField",
|
||||
"u_Texture", "u_MainDepth", "u_TranslucentDepth", "u_ItemEntityDepth", "u_ParticlesDepth", "u_WeatherDepth", "u_CloudsDepth"));
|
||||
|
|
@ -212,6 +214,8 @@ public enum LoVisualRenderPipelines {
|
|||
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
|
||||
public static final RenderPipeline UI_ROUNDED_FILL_SMOKE_BATCH = add(UiPipelines.batch("ui_rounded_fill_smoke_batch",
|
||||
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT, SHADER_UI_ROUNDED_FILL_SMOKE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
|
||||
public static final RenderPipeline UI_THEME_BACKDROP_BATCH = add(UiPipelines.batch("ui_theme_backdrop_batch",
|
||||
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT, SHADER_UI_THEME_BACKDROP_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
|
||||
public static final RenderPipeline UI_ROUNDED_STROKE_BATCH = add(UiPipelines.batch("ui_rounded_stroke_batch",
|
||||
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_STROKE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
|
||||
public static final RenderPipeline UI_ROUNDED_STROKE_ANGULAR_BATCH = add(UiPipelines.batch("ui_rounded_stroke_angular_batch",
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ public final class ShaderIds {
|
|||
public static final Identifier SHADER_ROUNDED_RECT_STROKE_CORNERS_BATCH_FRAG = shader("rounded_rect_stroke_corners_batch.frag");
|
||||
public static final Identifier SHADER_ROUNDED_RECT_BATCH_FRAG = shader("rounded_rect_batch.frag");
|
||||
public static final Identifier SHADER_UI_ROUNDED_FILL_SMOKE_BATCH_FRAG = shader("ui_rounded_fill_smoke_batch.frag");
|
||||
public static final Identifier SHADER_UI_THEME_BACKDROP_BATCH_FRAG = shader("ui_theme_backdrop_batch.frag");
|
||||
public static final Identifier SHADER_ROUNDED_RECT_STROKE_BATCH_FRAG = shader("rounded_rect_stroke_batch.frag");
|
||||
public static final Identifier SHADER_ROUNDED_RECT_STROKE_ANGULAR_BATCH_FRAG = shader("rounded_rect_stroke_angular_batch.frag");
|
||||
public static final Identifier SHADER_CHAMFERED_RECT_BATCH_FRAG = shader("chamfered_rect_batch.frag");
|
||||
|
|
@ -81,6 +82,7 @@ public final class ShaderIds {
|
|||
public static final Identifier SHADER_DAMAGE_TINT_FRAG = shader("damage_tint.frag");
|
||||
public static final Identifier SHADER_POST_FX_FRAG = shader("post_fx.frag");
|
||||
public static final Identifier SHADER_MOTION_BLUR_FRAG = shader("motion_blur.frag");
|
||||
public static final Identifier SHADER_GOD_RAYS_FRAG = shader("godrays.frag");
|
||||
public static final Identifier SHADER_DEPTH_OF_FIELD_FRAG = shader("depth_of_field.frag");
|
||||
public static final Identifier SHADER_WET_SURFACE_FRAG = shader("wet_surface.frag");
|
||||
public static final Identifier SHADER_HEAT_FX_FRAG = shader("heat_fx.frag");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
package dev.loki.lovisual.render.engine.renderer.renderer2d.fx.backdrop;
|
||||
|
||||
import dev.loki.lovisual.features.theme.Backdrop;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.renderer2d.Batch2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.DrawBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.UiBatchType;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
|
||||
/** Draws a theme's animated shader backdrop into a rounded rect, optionally as one side of a wipe. */
|
||||
public final class ThemeBackdrop2D {
|
||||
/** How a draw takes part in the theme-change wipe. */
|
||||
public enum Wipe {
|
||||
NONE(0.0f), REVEAL(1.0f), ERASE(2.0f);
|
||||
|
||||
private final float kind;
|
||||
|
||||
Wipe(float kind) {
|
||||
this.kind = kind;
|
||||
}
|
||||
}
|
||||
|
||||
private static final double CLOCK_WRAP_SECONDS = 4096.0;
|
||||
private static final float DARK_SHARE = 0.55f;
|
||||
private static final float LIGHT_SHARE = 0.45f;
|
||||
|
||||
private ThemeBackdrop2D() {
|
||||
}
|
||||
|
||||
/** The three ramp colours of a backdrop: a darkened accent, the accent, and the accent lifted towards the text colour. */
|
||||
public static int[] palette(int accent, int text) {
|
||||
return new int[]{scale(accent, DARK_SHARE), accent | 0xFF000000, mix(accent, text, LIGHT_SHARE)};
|
||||
}
|
||||
|
||||
public static float clock() {
|
||||
return (float) (System.nanoTime() / 1.0E9 % CLOCK_WRAP_SECONDS);
|
||||
}
|
||||
|
||||
public static void draw(Renderer2D r, double x, double y, double w, double h, float radius, Backdrop backdrop,
|
||||
int accent, int text, float opacity, Wipe wipe, float wipeProgress) {
|
||||
if (w <= 0.0 || h <= 0.0 || !backdrop.isActive() || opacity <= 0.0f) return;
|
||||
DrawBatch batch = Batch2D.open(r, UiBatchType.THEME_BACKDROP);
|
||||
if (batch == null) return;
|
||||
MeshBuilder mesh = batch.mesh;
|
||||
mesh.ensureQuadCapacity();
|
||||
|
||||
int[] colors = palette(accent, text);
|
||||
int[] first = Batch2D.rgba(colors[0]);
|
||||
float[] second = Batch2D.rgbaNorm(colors[1]);
|
||||
float[] third = Batch2D.rgbaNorm(colors[2]);
|
||||
first[3] = Math.round(Math.max(0.0f, Math.min(opacity, 1.0f)) * 255.0f);
|
||||
float time = clock();
|
||||
float mode = backdrop.mode().shaderIndex();
|
||||
|
||||
int i1 = vertex(mesh, x, y, x, y, w, h, first, second, third, radius, mode, backdrop, time, wipe, wipeProgress);
|
||||
int i2 = vertex(mesh, x, y + h, x, y, w, h, first, second, third, radius, mode, backdrop, time, wipe, wipeProgress);
|
||||
int i3 = vertex(mesh, x + w, y + h, x, y, w, h, first, second, third, radius, mode, backdrop, time, wipe, wipeProgress);
|
||||
int i4 = vertex(mesh, x + w, y, x, y, w, h, first, second, third, radius, mode, backdrop, time, wipe, wipeProgress);
|
||||
mesh.quad(i1, i2, i3, i4);
|
||||
Batch2D.close();
|
||||
}
|
||||
|
||||
private static int vertex(MeshBuilder mesh, double px, double py, double rx, double ry, double w, double h,
|
||||
int[] first, float[] second, float[] third, float radius, float mode, Backdrop backdrop,
|
||||
float time, Wipe wipe, float wipeProgress) {
|
||||
return mesh.vec2(px, py).local2(px, py).color(first[0], first[1], first[2], first[3])
|
||||
.vec4((float) rx, (float) ry, (float) w, (float) h)
|
||||
.vec4(radius, 0.0f, mode, backdrop.intensity())
|
||||
.vec4(second[0], second[1], second[2], second[3])
|
||||
.vec4(third[0], third[1], third[2], third[3])
|
||||
.vec4(time, backdrop.scale(), backdrop.speed(), 0.0f)
|
||||
.vec4(0.5f, 0.5f, wipeProgress, wipe.kind)
|
||||
.next();
|
||||
}
|
||||
|
||||
private static int scale(int argb, float factor) {
|
||||
int r = Math.round((argb >> 16 & 0xFF) * factor);
|
||||
int g = Math.round((argb >> 8 & 0xFF) * factor);
|
||||
int b = Math.round((argb & 0xFF) * factor);
|
||||
return 0xFF000000 | r << 16 | g << 8 | b;
|
||||
}
|
||||
|
||||
private static int mix(int from, int to, float t) {
|
||||
int r = Math.round((from >> 16 & 0xFF) + ((to >> 16 & 0xFF) - (from >> 16 & 0xFF)) * t);
|
||||
int g = Math.round((from >> 8 & 0xFF) + ((to >> 8 & 0xFF) - (from >> 8 & 0xFF)) * t);
|
||||
int b = Math.round((from & 0xFF) + ((to & 0xFF) - (from & 0xFF)) * t);
|
||||
return 0xFF000000 | r << 16 | g << 8 | b;
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ public enum UiBatchType {
|
|||
ORBIZ_RING(LoVisualRenderPipelines.UI_ORBIZ_RING_BATCH, true, false),
|
||||
ROUNDED(LoVisualRenderPipelines.UI_ROUNDED_BATCH, true, false),
|
||||
ROUNDED_FILL_SMOKE(LoVisualRenderPipelines.UI_ROUNDED_FILL_SMOKE_BATCH, true, false),
|
||||
THEME_BACKDROP(LoVisualRenderPipelines.UI_THEME_BACKDROP_BATCH, true, false),
|
||||
ROUNDED_STROKE(LoVisualRenderPipelines.UI_ROUNDED_STROKE_BATCH, true, false),
|
||||
ROUNDED_STROKE_ANGULAR(LoVisualRenderPipelines.UI_ROUNDED_STROKE_ANGULAR_BATCH, true, false),
|
||||
CHAMFERED(LoVisualRenderPipelines.UI_CHAMFERED_BATCH, true, false),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package dev.loki.lovisual.render.engine.uniform.impl.godrays;
|
||||
|
||||
import com.mojang.blaze3d.buffers.GpuBufferSlice;
|
||||
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||
import com.mojang.blaze3d.buffers.Std140SizeCalculator;
|
||||
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
|
||||
import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator;
|
||||
|
||||
public enum GodRaysUniforms {
|
||||
;
|
||||
public static final int SIZE = new Std140SizeCalculator()
|
||||
.putVec4().putVec4().putVec4().putVec4()
|
||||
.get();
|
||||
|
||||
private static final Data DATA = new Data();
|
||||
private static final String UNIFORM_NAME = "LoVisual - GodRays UBO";
|
||||
private static final int EXPECTED_WRITES_PER_FRAME = 2;
|
||||
|
||||
public static void update(float sunU, float sunV, float reach, int samples, float sampleWeight,
|
||||
float strength, float decay, float red, float green, float blue, float fade,
|
||||
int width, int height) {
|
||||
DATA.sunU = sunU;
|
||||
DATA.sunV = sunV;
|
||||
DATA.reach = reach;
|
||||
DATA.samples = samples;
|
||||
DATA.sampleWeight = sampleWeight;
|
||||
DATA.strength = strength;
|
||||
DATA.decay = decay;
|
||||
DATA.red = red;
|
||||
DATA.green = green;
|
||||
DATA.blue = blue;
|
||||
DATA.fade = fade;
|
||||
DATA.width = width;
|
||||
DATA.height = height;
|
||||
LoVisualRenderSystem.uniforms().write(UNIFORM_NAME, SIZE, EXPECTED_WRITES_PER_FRAME, DATA);
|
||||
}
|
||||
|
||||
public static GpuBufferSlice get() {
|
||||
return LoVisualRenderSystem.uniforms().current(UNIFORM_NAME);
|
||||
}
|
||||
|
||||
private static final class Data implements LoVisualUniformAllocator.UniformWriter {
|
||||
private float sunU, sunV, reach, samples;
|
||||
private float sampleWeight, strength, decay;
|
||||
private float red, green, blue, fade;
|
||||
private int width, height;
|
||||
|
||||
@Override
|
||||
public void write(java.nio.ByteBuffer buffer) {
|
||||
Std140Builder b = Std140Builder.intoBuffer(buffer);
|
||||
b.putFloat(sunU).putFloat(sunV).putFloat(reach).putFloat(0.0f);
|
||||
b.putFloat(samples).putFloat(sampleWeight).putFloat(strength).putFloat(decay);
|
||||
b.putFloat(red).putFloat(green).putFloat(blue).putFloat(fade);
|
||||
b.putFloat(width > 0 ? 1.0f / width : 0.0f).putFloat(height > 0 ? 1.0f / height : 0.0f)
|
||||
.putFloat(width).putFloat(height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
package dev.loki.lovisual.util.bedrock.anim;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* One animated vector channel (rotation, position or scale of a bone): keyframes with separate values arriving
|
||||
* at and leaving each key, blended linearly or, where a key asks for it, along a Catmull-Rom spline.
|
||||
*/
|
||||
public final class Channel {
|
||||
private final float[] times;
|
||||
private final float[] pre;
|
||||
private final float[] post;
|
||||
private final boolean[] smooth;
|
||||
|
||||
private Channel(float[] times, float[] pre, float[] post, boolean[] smooth) {
|
||||
this.times = times;
|
||||
this.pre = pre;
|
||||
this.post = post;
|
||||
this.smooth = smooth;
|
||||
}
|
||||
|
||||
public float lastTime() {
|
||||
return times[times.length - 1];
|
||||
}
|
||||
|
||||
/** Writes the three components at {@code time} into {@code out} starting at {@code at}. */
|
||||
public void sample(float time, float[] out, int at) {
|
||||
int last = times.length - 1;
|
||||
if (time <= times[0]) {
|
||||
copy(pre, 0, out, at);
|
||||
return;
|
||||
}
|
||||
if (time >= times[last]) {
|
||||
copy(post, last, out, at);
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
while (times[i + 1] <= time) i++;
|
||||
float span = times[i + 1] - times[i];
|
||||
if (span <= 0.0f) {
|
||||
copy(post, i, out, at);
|
||||
return;
|
||||
}
|
||||
float f = (time - times[i]) / span;
|
||||
if (smooth[i]) spline(i, f, out, at, last);
|
||||
else lerp(i, f, out, at);
|
||||
}
|
||||
|
||||
private void lerp(int i, float f, float[] out, int at) {
|
||||
for (int a = 0; a < 3; a++) {
|
||||
float from = post[i * 3 + a];
|
||||
out[at + a] = from + (pre[(i + 1) * 3 + a] - from) * f;
|
||||
}
|
||||
}
|
||||
|
||||
private void spline(int i, float f, float[] out, int at, int last) {
|
||||
int before = Math.max(i - 1, 0) * 3;
|
||||
int after = Math.min(i + 2, last) * 3;
|
||||
float f2 = f * f;
|
||||
float f3 = f2 * f;
|
||||
for (int a = 0; a < 3; a++) {
|
||||
float p0 = post[before + a];
|
||||
float p1 = post[i * 3 + a];
|
||||
float p2 = pre[(i + 1) * 3 + a];
|
||||
float p3 = pre[after + a];
|
||||
out[at + a] = 0.5f * (2.0f * p1 + (p2 - p0) * f
|
||||
+ (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * f2
|
||||
+ (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * f3);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copy(float[] from, int index, float[] out, int at) {
|
||||
System.arraycopy(from, index * 3, out, at, 3);
|
||||
}
|
||||
|
||||
/** A channel is a constant vector or a map of time to key; null when absent or empty. */
|
||||
public static Channel parse(JsonElement element) {
|
||||
if (element == null) return null;
|
||||
if (element.isJsonArray()) {
|
||||
float[] v = vector(element);
|
||||
return new Channel(new float[]{0.0f}, v.clone(), v, new boolean[]{false});
|
||||
}
|
||||
if (!element.isJsonObject()) return null;
|
||||
List<Object[]> keys = new ArrayList<>();
|
||||
for (Map.Entry<String, JsonElement> entry : element.getAsJsonObject().entrySet()) {
|
||||
float time;
|
||||
try {
|
||||
time = Float.parseFloat(entry.getKey());
|
||||
} catch (NumberFormatException e) {
|
||||
continue;
|
||||
}
|
||||
keys.add(new Object[]{time, entry.getValue()});
|
||||
}
|
||||
if (keys.isEmpty()) return null;
|
||||
keys.sort((a, b) -> Float.compare((Float) a[0], (Float) b[0]));
|
||||
int n = keys.size();
|
||||
float[] times = new float[n];
|
||||
float[] pre = new float[n * 3];
|
||||
float[] post = new float[n * 3];
|
||||
boolean[] smooth = new boolean[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
times[i] = (Float) keys.get(i)[0];
|
||||
JsonElement key = (JsonElement) keys.get(i)[1];
|
||||
float[] arriving;
|
||||
float[] leaving;
|
||||
if (key.isJsonObject()) {
|
||||
JsonObject object = key.getAsJsonObject();
|
||||
leaving = object.has("post") ? vector(object.get("post")) : object.has("vector") ? vector(object.get("vector")) : null;
|
||||
arriving = object.has("pre") ? vector(object.get("pre")) : null;
|
||||
if (leaving == null) leaving = arriving == null ? new float[3] : arriving;
|
||||
if (arriving == null) arriving = leaving;
|
||||
smooth[i] = object.has("lerp_mode") && "catmullrom".equals(object.get("lerp_mode").getAsString());
|
||||
} else {
|
||||
leaving = vector(key);
|
||||
arriving = leaving;
|
||||
}
|
||||
System.arraycopy(arriving, 0, pre, i * 3, 3);
|
||||
System.arraycopy(leaving, 0, post, i * 3, 3);
|
||||
}
|
||||
return new Channel(times, pre, post, smooth);
|
||||
}
|
||||
|
||||
private static float[] vector(JsonElement value) {
|
||||
float[] out = new float[3];
|
||||
if (value.isJsonArray()) {
|
||||
JsonArray array = value.getAsJsonArray();
|
||||
for (int i = 0; i < Math.min(3, array.size()); i++) out[i] = number(array.get(i));
|
||||
} else if (value.isJsonObject()) {
|
||||
JsonObject object = value.getAsJsonObject();
|
||||
for (String key : new String[]{"post", "vector", "pre"}) {
|
||||
if (object.has(key)) return vector(object.get(key));
|
||||
}
|
||||
} else {
|
||||
java.util.Arrays.fill(out, number(value));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static float number(JsonElement element) {
|
||||
try {
|
||||
return element.getAsFloat();
|
||||
} catch (RuntimeException e) {
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
124
mod/src/main/java/dev/loki/lovisual/util/bedrock/anim/Clip.java
Normal file
124
mod/src/main/java/dev/loki/lovisual/util/bedrock/anim/Clip.java
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package dev.loki.lovisual.util.bedrock.anim;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** One Bedrock animation: its length, looping, per-bone channels and timed sound events. */
|
||||
public final class Clip {
|
||||
/** The channels of one bone; any of them may be null. */
|
||||
public record Tracks(Channel rotation, Channel position, Channel scale) {
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final float length;
|
||||
private final boolean loop;
|
||||
private final Map<String, Tracks> bones;
|
||||
private final float[] soundTimes;
|
||||
private final String[] soundNames;
|
||||
|
||||
private Clip(String name, float length, boolean loop, Map<String, Tracks> bones, float[] soundTimes, String[] soundNames) {
|
||||
this.name = name;
|
||||
this.length = length;
|
||||
this.loop = loop;
|
||||
this.bones = bones;
|
||||
this.soundTimes = soundTimes;
|
||||
this.soundNames = soundNames;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public float length() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public boolean loops() {
|
||||
return loop;
|
||||
}
|
||||
|
||||
public Map<String, Tracks> bones() {
|
||||
return bones;
|
||||
}
|
||||
|
||||
public int soundCount() {
|
||||
return soundTimes.length;
|
||||
}
|
||||
|
||||
public float soundTime(int index) {
|
||||
return soundTimes[index];
|
||||
}
|
||||
|
||||
public String soundName(int index) {
|
||||
return soundNames[index];
|
||||
}
|
||||
|
||||
/** Every clip of an animation file, keyed by name without its {@code animation.<model>.} prefix. */
|
||||
public static Map<String, Clip> parseAll(String json) {
|
||||
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
|
||||
JsonObject clips = root.has("animations") ? root.getAsJsonObject("animations") : root;
|
||||
Map<String, Clip> out = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, JsonElement> entry : clips.entrySet()) {
|
||||
if (!entry.getValue().isJsonObject()) continue;
|
||||
JsonObject clip = entry.getValue().getAsJsonObject();
|
||||
if (!clip.has("bones")) continue;
|
||||
String name = entry.getKey().substring(entry.getKey().lastIndexOf('.') + 1);
|
||||
out.put(name, parse(name, clip));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Clip parse(String name, JsonObject clip) {
|
||||
Map<String, Tracks> bones = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, JsonElement> entry : clip.getAsJsonObject("bones").entrySet()) {
|
||||
if (!entry.getValue().isJsonObject()) continue;
|
||||
JsonObject channels = entry.getValue().getAsJsonObject();
|
||||
bones.put(entry.getKey(), new Tracks(Channel.parse(channels.get("rotation")),
|
||||
Channel.parse(channels.get("position")), Channel.parse(channels.get("scale"))));
|
||||
}
|
||||
float length = clip.has("animation_length") ? clip.get("animation_length").getAsFloat() : 0.0f;
|
||||
if (length <= 0.0f) length = lastKey(bones);
|
||||
boolean loop = !clip.has("loop") || !clip.get("loop").isJsonPrimitive() || !clip.get("loop").getAsJsonPrimitive().isBoolean()
|
||||
|| clip.get("loop").getAsBoolean();
|
||||
|
||||
List<float[]> sounds = new ArrayList<>();
|
||||
List<String> names = new ArrayList<>();
|
||||
if (clip.has("sound_effects") && clip.get("sound_effects").isJsonObject()) {
|
||||
for (Map.Entry<String, JsonElement> entry : clip.getAsJsonObject("sound_effects").entrySet()) {
|
||||
JsonElement value = entry.getValue();
|
||||
if (!value.isJsonObject() || !value.getAsJsonObject().has("effect")) continue;
|
||||
try {
|
||||
sounds.add(new float[]{Float.parseFloat(entry.getKey())});
|
||||
names.add(value.getAsJsonObject().get("effect").getAsString());
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
float[] times = new float[sounds.size()];
|
||||
String[] effects = new String[times.length];
|
||||
Integer[] order = new Integer[times.length];
|
||||
for (int i = 0; i < order.length; i++) order[i] = i;
|
||||
java.util.Arrays.sort(order, (a, b) -> Float.compare(sounds.get(a)[0], sounds.get(b)[0]));
|
||||
for (int i = 0; i < order.length; i++) {
|
||||
times[i] = sounds.get(order[i])[0];
|
||||
effects[i] = names.get(order[i]);
|
||||
}
|
||||
return new Clip(name, length, loop, bones, times, effects);
|
||||
}
|
||||
|
||||
private static float lastKey(Map<String, Tracks> bones) {
|
||||
float last = 0.0f;
|
||||
for (Tracks tracks : bones.values()) {
|
||||
for (Channel channel : new Channel[]{tracks.rotation(), tracks.position(), tracks.scale()}) {
|
||||
if (channel != null) last = Math.max(last, channel.lastTime());
|
||||
}
|
||||
}
|
||||
return last;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package dev.loki.lovisual.util.bedrock.geo;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* One bone of a Bedrock model. Pivots and offsets are in model pixels, rotations in radians, already in the
|
||||
* entity frame (x mirrored). The {@code rot/offset/scale} arrays are the animated pose, the {@code base*}
|
||||
* ones the pose stored in the file.
|
||||
*/
|
||||
public final class GeoBone {
|
||||
public final String name;
|
||||
public final float[] pivot;
|
||||
public final float[] baseRot;
|
||||
public final float[] rot = new float[3];
|
||||
public final float[] offset = new float[3];
|
||||
public final float[] scale = {1.0f, 1.0f, 1.0f};
|
||||
public final List<GeoCube> cubes = new ArrayList<>();
|
||||
public final List<GeoBone> children = new ArrayList<>();
|
||||
|
||||
GeoBone(String name, float[] pivot, float[] baseRot) {
|
||||
this.name = name;
|
||||
this.pivot = pivot;
|
||||
this.baseRot = baseRot;
|
||||
resetPose();
|
||||
}
|
||||
|
||||
public void resetPose() {
|
||||
System.arraycopy(baseRot, 0, rot, 0, 3);
|
||||
java.util.Arrays.fill(offset, 0.0f);
|
||||
java.util.Arrays.fill(scale, 1.0f);
|
||||
}
|
||||
|
||||
/** Applies this bone's pose to {@code matrix}: offset, then rotation and scale about the pivot. */
|
||||
public Matrix4f transform(Matrix4f matrix) {
|
||||
return matrix.translate(offset[0] / 16.0f, offset[1] / 16.0f, offset[2] / 16.0f)
|
||||
.translate(pivot[0] / 16.0f, pivot[1] / 16.0f, pivot[2] / 16.0f)
|
||||
.rotateZYX(rot[2], rot[1], rot[0])
|
||||
.scale(scale[0], scale[1], scale[2])
|
||||
.translate(-pivot[0] / 16.0f, -pivot[1] / 16.0f, -pivot[2] / 16.0f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package dev.loki.lovisual.util.bedrock.geo;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A cube of a bone: its own pivot and rotation (radians) plus the baked textured faces in model pixels.
|
||||
* Faces are stored as four corners (x, y, z, u, v each, in texture pixels) in drawing order.
|
||||
*/
|
||||
public record GeoCube(float[] pivot, float[] rot, List<float[]> faces) {
|
||||
|
||||
public boolean rotated() {
|
||||
return rot[0] != 0.0f || rot[1] != 0.0f || rot[2] != 0.0f;
|
||||
}
|
||||
|
||||
/** Applies the cube's own rotation about its pivot to {@code matrix} (blocks). */
|
||||
public Matrix4f transform(Matrix4f matrix) {
|
||||
return matrix.translate(pivot[0] / 16.0f, pivot[1] / 16.0f, pivot[2] / 16.0f)
|
||||
.rotateZYX(rot[2], rot[1], rot[0])
|
||||
.translate(-pivot[0] / 16.0f, -pivot[1] / 16.0f, -pivot[2] / 16.0f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package dev.loki.lovisual.util.bedrock.geo;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Turns a Bedrock cube (origin, size, inflate, box or per-face uv) into textured quads in the entity frame,
|
||||
* where x is mirrored. Every texture runs towards the viewer's right and down as seen from outside the face,
|
||||
* so a painted texture reads the same way it does in Blockbench.
|
||||
*/
|
||||
final class GeoFaces {
|
||||
/** Face order: north, south, east, west, up, down. */
|
||||
private static final String[] NAMES = {"north", "south", "east", "west", "up", "down"};
|
||||
/** Per face: viewer-right axis, viewer-down axis, outward normal axis (as signed unit vectors). */
|
||||
private static final float[][] AXES = {
|
||||
{-1, 0, 0, 0, -1, 0, 0, 0, -1},
|
||||
{1, 0, 0, 0, -1, 0, 0, 0, 1},
|
||||
{0, 0, 1, 0, -1, 0, -1, 0, 0},
|
||||
{0, 0, -1, 0, -1, 0, 1, 0, 0},
|
||||
{-1, 0, 0, 0, 0, -1, 0, 1, 0},
|
||||
{1, 0, 0, 0, 0, -1, 0, -1, 0},
|
||||
};
|
||||
|
||||
private GeoFaces() {
|
||||
}
|
||||
|
||||
static List<float[]> build(JsonObject cube, float[] origin, float[] size, float inflate) {
|
||||
float[] centre = new float[3];
|
||||
float[] half = new float[3];
|
||||
// x is mirrored into the entity frame
|
||||
centre[0] = -(origin[0] + size[0] / 2.0f);
|
||||
centre[1] = origin[1] + size[1] / 2.0f;
|
||||
centre[2] = origin[2] + size[2] / 2.0f;
|
||||
half[0] = size[0] / 2.0f + inflate;
|
||||
half[1] = size[1] / 2.0f + inflate;
|
||||
half[2] = size[2] / 2.0f + inflate;
|
||||
|
||||
float[][] regions = regions(cube, size);
|
||||
// a flat cube has two coincident planes; drawing both would z-fight, and the faces are double sided anyway
|
||||
dropTwin(regions, 0, 1, half[2]);
|
||||
dropTwin(regions, 2, 3, half[0]);
|
||||
dropTwin(regions, 4, 5, half[1]);
|
||||
List<float[]> faces = new ArrayList<>();
|
||||
for (int face = 0; face < 6; face++) {
|
||||
float[] region = regions[face];
|
||||
if (region == null) continue;
|
||||
float[] axes = AXES[face];
|
||||
float[] extent = extent(face, half);
|
||||
if (extent[0] <= 0.0f || extent[1] <= 0.0f) continue;
|
||||
faces.add(quad(centre, half, axes, extent, region));
|
||||
}
|
||||
return faces;
|
||||
}
|
||||
|
||||
private static void dropTwin(float[][] regions, int keep, int twin, float halfThickness) {
|
||||
if (halfThickness > 0.0f) return;
|
||||
if (regions[keep] != null) regions[twin] = null;
|
||||
}
|
||||
|
||||
/** Half extents of a face along its right and down axes. */
|
||||
private static float[] extent(int face, float[] half) {
|
||||
return switch (face) {
|
||||
case 0, 1 -> new float[]{half[0], half[1]};
|
||||
case 2, 3 -> new float[]{half[2], half[1]};
|
||||
default -> new float[]{half[0], half[2]};
|
||||
};
|
||||
}
|
||||
|
||||
private static float[] quad(float[] centre, float[] half, float[] axes, float[] extent, float[] region) {
|
||||
float[] out = new float[20];
|
||||
float[][] corners = {{-1, -1}, {1, -1}, {1, 1}, {-1, 1}};
|
||||
int rotation = Math.floorMod((int) region[4] / 90, 4);
|
||||
float depth = Math.abs(axes[6]) * half[0] + Math.abs(axes[7]) * half[1] + Math.abs(axes[8]) * half[2];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float sx = corners[i][0] * extent[0];
|
||||
float sy = corners[i][1] * extent[1];
|
||||
for (int a = 0; a < 3; a++) {
|
||||
out[i * 5 + a] = centre[a] + axes[a] * sx + axes[3 + a] * sy + axes[6 + a] * depth;
|
||||
}
|
||||
// uv_rotation turns the texture a quarter turn per step by shifting which corner gets which uv
|
||||
float[] uv = corner(region, (i + rotation) % 4);
|
||||
out[i * 5 + 3] = uv[0];
|
||||
out[i * 5 + 4] = uv[1];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static float[] corner(float[] region, int index) {
|
||||
float u0 = region[0];
|
||||
float v0 = region[1];
|
||||
float u1 = u0 + region[2];
|
||||
float v1 = v0 + region[3];
|
||||
return switch (index) {
|
||||
case 0 -> new float[]{u0, v0};
|
||||
case 1 -> new float[]{u1, v0};
|
||||
case 2 -> new float[]{u1, v1};
|
||||
default -> new float[]{u0, v1};
|
||||
};
|
||||
}
|
||||
|
||||
/** Per face {u, v, width, height, rotation} in texture pixels, or null when the cube has no such face. */
|
||||
private static float[][] regions(JsonObject cube, float[] size) {
|
||||
float[][] out = new float[6][];
|
||||
JsonElement uv = cube.get("uv");
|
||||
if (uv == null) return out;
|
||||
if (uv.isJsonArray()) {
|
||||
boxRegions(out, uv.getAsJsonArray(), size, cube.has("mirror") && cube.get("mirror").getAsBoolean());
|
||||
} else if (uv.isJsonObject()) {
|
||||
JsonObject faces = uv.getAsJsonObject();
|
||||
for (int face = 0; face < 6; face++) {
|
||||
if (!faces.has(NAMES[face])) continue;
|
||||
JsonObject data = faces.getAsJsonObject(NAMES[face]);
|
||||
JsonArray at = data.getAsJsonArray("uv");
|
||||
JsonArray span = data.has("uv_size") ? data.getAsJsonArray("uv_size") : null;
|
||||
float w = span == null ? 0 : span.get(0).getAsFloat();
|
||||
float h = span == null ? 0 : span.get(1).getAsFloat();
|
||||
float rotation = data.has("uv_rotation") ? data.get("uv_rotation").getAsFloat() : 0;
|
||||
out[face] = new float[]{at.get(0).getAsFloat(), at.get(1).getAsFloat(), w, h, rotation};
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static void boxRegions(float[][] out, JsonArray uv, float[] size, boolean mirror) {
|
||||
float u = uv.get(0).getAsFloat();
|
||||
float v = uv.get(1).getAsFloat();
|
||||
float w = size[0];
|
||||
float h = size[1];
|
||||
float d = size[2];
|
||||
float[] east = {u, v + d, d, h, 0};
|
||||
float[] west = {u + d + w, v + d, d, h, 0};
|
||||
out[0] = new float[]{u + d, v + d, w, h, 0};
|
||||
out[1] = new float[]{u + 2 * d + w, v + d, w, h, 0};
|
||||
out[2] = mirror ? west : east;
|
||||
out[3] = mirror ? east : west;
|
||||
out[4] = new float[]{u + d, v, w, d, 0};
|
||||
out[5] = new float[]{u + d + w, v + d, w, -d, 0};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package dev.loki.lovisual.util.bedrock.geo;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Reads a Bedrock {@code .geo.json} (formats 1.12 and 1.21) into a bone tree. Pivots, origins and rotations
|
||||
* are moved into the entity frame: x is mirrored, and rotations become (-x, -y, z) radians.
|
||||
*/
|
||||
public final class GeoParser {
|
||||
/** A parsed model: the root bones, every bone by name and the texture size the uv are measured in. */
|
||||
public record Model(List<GeoBone> roots, Map<String, GeoBone> bones, int textureWidth, int textureHeight) {
|
||||
public void resetPose() {
|
||||
for (GeoBone bone : bones.values()) bone.resetPose();
|
||||
}
|
||||
}
|
||||
|
||||
private GeoParser() {
|
||||
}
|
||||
|
||||
public static Model parse(String json) {
|
||||
JsonObject geometry = JsonParser.parseString(json).getAsJsonObject()
|
||||
.getAsJsonArray("minecraft:geometry").get(0).getAsJsonObject();
|
||||
JsonObject description = geometry.getAsJsonObject("description");
|
||||
int width = description.has("texture_width") ? description.get("texture_width").getAsInt() : 64;
|
||||
int height = description.has("texture_height") ? description.get("texture_height").getAsInt() : 64;
|
||||
|
||||
Map<String, GeoBone> bones = new LinkedHashMap<>();
|
||||
Map<String, String> parents = new LinkedHashMap<>();
|
||||
for (JsonElement entry : geometry.getAsJsonArray("bones")) {
|
||||
JsonObject data = entry.getAsJsonObject();
|
||||
String name = data.get("name").getAsString();
|
||||
GeoBone bone = new GeoBone(name, pivot(data), rotation(data));
|
||||
if (data.has("cubes")) {
|
||||
for (JsonElement cube : data.getAsJsonArray("cubes")) bone.cubes.add(cube(cube.getAsJsonObject()));
|
||||
}
|
||||
bones.put(name, bone);
|
||||
if (data.has("parent")) parents.put(name, data.get("parent").getAsString());
|
||||
}
|
||||
List<GeoBone> roots = new ArrayList<>();
|
||||
for (GeoBone bone : bones.values()) {
|
||||
GeoBone parent = bones.get(parents.get(bone.name));
|
||||
if (parent == null || parent == bone) roots.add(bone);
|
||||
else parent.children.add(bone);
|
||||
}
|
||||
return new Model(roots, bones, width, height);
|
||||
}
|
||||
|
||||
private static GeoCube cube(JsonObject data) {
|
||||
float[] origin = vec(data, "origin", 0.0f);
|
||||
float[] size = vec(data, "size", 0.0f);
|
||||
float inflate = data.has("inflate") ? data.get("inflate").getAsFloat() : 0.0f;
|
||||
float[] pivot = data.has("pivot") ? vec(data, "pivot", 0.0f)
|
||||
: new float[]{origin[0] + size[0] / 2.0f, origin[1] + size[1] / 2.0f, origin[2] + size[2] / 2.0f};
|
||||
float[] pivotMirrored = {-pivot[0], pivot[1], pivot[2]};
|
||||
return new GeoCube(pivotMirrored, rotation(data), GeoFaces.build(data, origin, size, inflate));
|
||||
}
|
||||
|
||||
private static float[] pivot(JsonObject data) {
|
||||
float[] pivot = vec(data, "pivot", 0.0f);
|
||||
return new float[]{-pivot[0], pivot[1], pivot[2]};
|
||||
}
|
||||
|
||||
private static float[] rotation(JsonObject data) {
|
||||
float[] degrees = vec(data, "rotation", 0.0f);
|
||||
return new float[]{(float) Math.toRadians(-degrees[0]), (float) Math.toRadians(-degrees[1]), (float) Math.toRadians(degrees[2])};
|
||||
}
|
||||
|
||||
private static float[] vec(JsonObject data, String key, float fallback) {
|
||||
float[] out = {fallback, fallback, fallback};
|
||||
if (!data.has(key) || !data.get(key).isJsonArray()) return out;
|
||||
JsonArray array = data.getAsJsonArray(key);
|
||||
for (int i = 0; i < Math.min(3, array.size()); i++) out[i] = array.get(i).getAsFloat();
|
||||
return out;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
659
mod/src/main/resources/assets/lovisual/knife/bayonet.geo.json
Normal file
659
mod/src/main/resources/assets/lovisual/knife/bayonet.geo.json
Normal file
|
|
@ -0,0 +1,659 @@
|
|||
{
|
||||
"format_version": "1.12.0",
|
||||
"minecraft:geometry": [
|
||||
{
|
||||
"description": {
|
||||
"identifier": "geometry.unknown",
|
||||
"texture_width": 32,
|
||||
"texture_height": 32,
|
||||
"visible_bounds_width": 4,
|
||||
"visible_bounds_height": 4,
|
||||
"visible_bounds_offset": [0, 0, 0]
|
||||
},
|
||||
"bones": [
|
||||
{
|
||||
"name": "root",
|
||||
"pivot": [0, 9.6875, 4.3375]
|
||||
},
|
||||
{
|
||||
"name": "bone",
|
||||
"parent": "root",
|
||||
"pivot": [0, 9.6875, 4.3375]
|
||||
},
|
||||
{
|
||||
"name": "knife_and_right",
|
||||
"parent": "bone",
|
||||
"pivot": [6, 19, 0]
|
||||
},
|
||||
{
|
||||
"name": "bayonet",
|
||||
"parent": "knife_and_right",
|
||||
"pivot": [0, 9.5, 2.5]
|
||||
},
|
||||
{
|
||||
"name": "weapon",
|
||||
"parent": "bayonet",
|
||||
"pivot": [0, 9.5, -0.375]
|
||||
},
|
||||
{
|
||||
"name": "blade",
|
||||
"parent": "weapon",
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"cubes": [
|
||||
{
|
||||
"origin": [-2.20119, 8.6375, -2.86369],
|
||||
"size": [7.17385, 1.8699, 0.28865],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [0, 0], "uv_size": [7.25, 1.75]},
|
||||
"east": {"uv": [6, 17], "uv_size": [0.25, 1.75]},
|
||||
"south": {"uv": [0, 2], "uv_size": [7.25, 1.75]},
|
||||
"west": {"uv": [7, 17], "uv_size": [0.25, 1.75]},
|
||||
"up": {"uv": [0, 12], "uv_size": [7.25, 0.25]},
|
||||
"down": {"uv": [12, 0.25], "uv_size": [7.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-2.20119, 8.4676, -2.86369],
|
||||
"size": [0.92385, 0.1699, 0.28865],
|
||||
"pivot": [0, 8.163, -2.71994],
|
||||
"rotation": [0, 90, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [18, 13], "uv_size": [1, 0.25]},
|
||||
"east": {"uv": [21, 17], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [14, 18], "uv_size": [1, 0.25]},
|
||||
"west": {"uv": [18, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [18, 14], "uv_size": [1, 0.25]},
|
||||
"down": {"uv": [15, 18.25], "uv_size": [1, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [5.81387, 7.6926, -4.86369],
|
||||
"size": [0.8625, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -4.71994],
|
||||
"rotation": [-90, 80, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [18, 15], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [16, 20], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [16, 18], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [20, 16], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [18, 16], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [17, 18.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [6.16855, 6.56882, -4.86369],
|
||||
"size": [0.73672, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -4.71994],
|
||||
"rotation": [-90, 70, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [18, 17], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [17, 20], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [18, 18], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [20, 17], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [0, 19], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [1, 19.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [5.77341, 4.89964, -4.86369],
|
||||
"size": [0.83555, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -4.71994],
|
||||
"rotation": [-90, 55, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [19, 1], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [18, 20], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [2, 19], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [20, 18], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [3, 19], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [4, 19.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-3.33869, 8.94455, -5.80829],
|
||||
"size": [8.325, 0.2033, 0.2033],
|
||||
"pivot": [0, 10.0329, -4.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [6, 10], "uv_size": [8.25, 0.25]},
|
||||
"east": {"uv": [19, 20], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [10, 8], "uv_size": [8.25, 0.25]},
|
||||
"west": {"uv": [20, 19], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [10, 9], "uv_size": [8.25, 0.25]},
|
||||
"down": {"uv": [6, 11.25], "uv_size": [8.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [5.81387, 8.27641, -6.47643],
|
||||
"size": [0.8625, 0.2033, 0.2033],
|
||||
"pivot": [0, 10.0329, -4.71994],
|
||||
"rotation": [-45, 80, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [5, 19], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [20, 20], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [6, 19], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [0, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [7, 19], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [19, 7.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [6.16855, 7.48178, -7.27107],
|
||||
"size": [0.73672, 0.20329, 0.2033],
|
||||
"pivot": [0, 10.0329, -4.71994],
|
||||
"rotation": [-45, 70, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [8, 19], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [21, 0], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [19, 8], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [1, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [9, 19], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [19, 9.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [5.71387, 6.33693, -1.22599],
|
||||
"size": [0.83554, 0.20329, 0.2033],
|
||||
"pivot": [-0.05955, 9.98413, -4.7541],
|
||||
"rotation": [-135, 55, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [10, 19], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [21, 1], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [11, 19], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [2, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [12, 19], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [13, 19.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [5.80726, 10.7815, -10.72829],
|
||||
"size": [0.20328, 0.21563, 0.2033],
|
||||
"pivot": [0, 10.1649, -4.71773],
|
||||
"rotation": [11.2413, 44.4451, 7.9231],
|
||||
"uv": {
|
||||
"north": {"uv": [21, 2], "uv_size": [0.25, 0.25]},
|
||||
"east": {"uv": [3, 21], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [21, 3], "uv_size": [0.25, 0.25]},
|
||||
"west": {"uv": [4, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [21, 4], "uv_size": [0.25, 0.25]},
|
||||
"down": {"uv": [5, 21.25], "uv_size": [0.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [7.99708, 10.78151, -4.86148],
|
||||
"size": [0.35937, 0.21563, 0.2875],
|
||||
"pivot": [0, 10.1649, -4.71773],
|
||||
"rotation": [90, 82, 90],
|
||||
"uv": {
|
||||
"north": {"uv": [21, 5], "uv_size": [0.25, 0.25]},
|
||||
"east": {"uv": [6, 21], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [21, 6], "uv_size": [0.25, 0.25]},
|
||||
"west": {"uv": [7, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [21, 7], "uv_size": [0.25, 0.25]},
|
||||
"down": {"uv": [8, 21.25], "uv_size": [0.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [0.14375, 10.14688, -8.55625],
|
||||
"size": [4.52812, 0.35937, 0.2875],
|
||||
"pivot": [0.14375, 10.50625, -8.26875],
|
||||
"rotation": [90, 82, 90],
|
||||
"uv": {
|
||||
"north": {"uv": [15, 3], "uv_size": [4.5, 0.25]},
|
||||
"east": {"uv": [21, 8], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [15, 4], "uv_size": [4.5, 0.25]},
|
||||
"west": {"uv": [9, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [15, 5], "uv_size": [4.5, 0.25]},
|
||||
"down": {"uv": [15, 6.25], "uv_size": [4.5, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [2.97366, 8.63735, -4.86384],
|
||||
"size": [5.4628, 1.86905, 0.2878],
|
||||
"inflate": 0.001,
|
||||
"pivot": [0, 10.0329, -4.71994],
|
||||
"rotation": [0, 90, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [5.5, 4], "uv_size": [-5.5, 1.75]},
|
||||
"east": {"uv": [17, 10], "uv_size": [0.25, 1.75]},
|
||||
"south": {"uv": [5.5, 6], "uv_size": [-5.5, 1.75]},
|
||||
"up": {"uv": [18.5, 7], "uv_size": [-5.5, 0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [4.84571, 8.64101, -6.11183],
|
||||
"size": [1.15, 0.20329, 0.20329],
|
||||
"pivot": [0, 10.0329, -4.71994],
|
||||
"rotation": [-45, 85, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [18, 2], "uv_size": [1.25, 0.25]},
|
||||
"east": {"uv": [21, 9], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [3, 18], "uv_size": [1.25, 0.25]},
|
||||
"west": {"uv": [10, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [8, 18], "uv_size": [1.25, 0.25]},
|
||||
"down": {"uv": [10, 18.25], "uv_size": [1.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [4.84571, 8.20823, -4.86369],
|
||||
"size": [1.15, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -4.71994],
|
||||
"rotation": [-90, 85, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [18, 10], "uv_size": [1.25, 0.25]},
|
||||
"east": {"uv": [21, 10], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [18, 11], "uv_size": [1.25, 0.25]},
|
||||
"west": {"uv": [11, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [12, 18], "uv_size": [1.25, 0.25]},
|
||||
"down": {"uv": [18, 12.25], "uv_size": [1.25, -0.25]}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "grip",
|
||||
"parent": "weapon",
|
||||
"pivot": [0, 9.49999, 3.22902],
|
||||
"cubes": [
|
||||
{
|
||||
"origin": [-9.24494, 9.15295, -3.59989],
|
||||
"size": [6.75625, 1.00625, 1.00625],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [6, 4], "uv_size": [6.75, 1]},
|
||||
"east": {"uv": [12, 3], "uv_size": [1, 1]},
|
||||
"south": {"uv": [6, 5], "uv_size": [6.75, 1]},
|
||||
"west": {"uv": [4, 15], "uv_size": [1, 1]},
|
||||
"up": {"uv": [6, 6], "uv_size": [6.75, 1]},
|
||||
"down": {"uv": [6, 8], "uv_size": [6.75, -1]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-3.63869, 9.08108, -3.67176],
|
||||
"size": [1.15, 1.15, 1.15],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [12, 1], "uv_size": [1.25, 1.25]},
|
||||
"east": {"uv": [8, 12], "uv_size": [1.25, 1.25]},
|
||||
"south": {"uv": [10, 12], "uv_size": [1.25, 1.25]},
|
||||
"west": {"uv": [12, 12], "uv_size": [1.25, 1.25]},
|
||||
"up": {"uv": [0, 13], "uv_size": [1.25, 1.25]},
|
||||
"down": {"uv": [2, 14.25], "uv_size": [1.25, -1.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-4.78869, 9.08108, -3.67176],
|
||||
"size": [0.8625, 1.15, 1.15],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [15, 12], "uv_size": [0.75, 1.25]},
|
||||
"east": {"uv": [13, 3], "uv_size": [1.25, 1.25]},
|
||||
"south": {"uv": [15, 14], "uv_size": [0.75, 1.25]},
|
||||
"west": {"uv": [4, 13], "uv_size": [1.25, 1.25]},
|
||||
"up": {"uv": [0, 16], "uv_size": [0.75, 1.25]},
|
||||
"down": {"uv": [1, 17.25], "uv_size": [0.75, -1.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-5.93869, 9.08108, -3.67176],
|
||||
"size": [0.8625, 1.15, 1.15],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [16, 1], "uv_size": [0.75, 1.25]},
|
||||
"east": {"uv": [13, 5], "uv_size": [1.25, 1.25]},
|
||||
"south": {"uv": [2, 16], "uv_size": [0.75, 1.25]},
|
||||
"west": {"uv": [6, 13], "uv_size": [1.25, 1.25]},
|
||||
"up": {"uv": [3, 16], "uv_size": [0.75, 1.25]},
|
||||
"down": {"uv": [4, 17.25], "uv_size": [0.75, -1.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-7.08869, 9.08108, -3.67176],
|
||||
"size": [0.8625, 1.15, 1.15],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [8, 16], "uv_size": [0.75, 1.25]},
|
||||
"east": {"uv": [14, 1], "uv_size": [1.25, 1.25]},
|
||||
"south": {"uv": [9, 16], "uv_size": [0.75, 1.25]},
|
||||
"west": {"uv": [8, 14], "uv_size": [1.25, 1.25]},
|
||||
"up": {"uv": [10, 16], "uv_size": [0.75, 1.25]},
|
||||
"down": {"uv": [16, 11.25], "uv_size": [0.75, -1.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-9.10119, 8.93733, -3.81551],
|
||||
"size": [1.4375, 1.4375, 1.4375],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [8, 8], "uv_size": [1.5, 1.5]},
|
||||
"east": {"uv": [0, 10], "uv_size": [1.5, 1.5]},
|
||||
"south": {"uv": [10, 0], "uv_size": [1.5, 1.5]},
|
||||
"west": {"uv": [2, 10], "uv_size": [1.5, 1.5]},
|
||||
"up": {"uv": [10, 2], "uv_size": [1.5, 1.5]},
|
||||
"down": {"uv": [4, 11.5], "uv_size": [1.5, -1.5]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-8.23869, 9.08108, -3.67176],
|
||||
"size": [0.8625, 1.15, 1.15],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [11, 16], "uv_size": [0.75, 1.25]},
|
||||
"east": {"uv": [10, 14], "uv_size": [1.25, 1.25]},
|
||||
"south": {"uv": [12, 16], "uv_size": [0.75, 1.25]},
|
||||
"west": {"uv": [12, 14], "uv_size": [1.25, 1.25]},
|
||||
"up": {"uv": [16, 12], "uv_size": [0.75, 1.25]},
|
||||
"down": {"uv": [13, 17.25], "uv_size": [0.75, -1.25]}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bone3",
|
||||
"parent": "weapon",
|
||||
"pivot": [0, 0.875, -1.09375],
|
||||
"cubes": [
|
||||
{
|
||||
"origin": [0.8625, 7.27188, -0.80625],
|
||||
"size": [0.2875, 1.94062, 0.575],
|
||||
"pivot": [0.8625, 9.2125, -0.23125],
|
||||
"rotation": [0, 90, 15],
|
||||
"uv": {
|
||||
"north": {"uv": [14, 16], "uv_size": [0.25, 2]},
|
||||
"east": {"uv": [5, 15], "uv_size": [0.5, 2]},
|
||||
"south": {"uv": [16, 14], "uv_size": [0.25, 2]},
|
||||
"west": {"uv": [6, 15], "uv_size": [0.5, 2]},
|
||||
"up": {"uv": [11, 20], "uv_size": [0.25, 0.5]},
|
||||
"down": {"uv": [20, 11.5], "uv_size": [0.25, -0.5]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-0.8625, 7.27188, -1.09375],
|
||||
"size": [0.2875, 1.94062, 0.575],
|
||||
"pivot": [-0.8625, 9.2125, -0.51875],
|
||||
"rotation": [0, -90, -15],
|
||||
"uv": {
|
||||
"north": {"uv": [15, 16], "uv_size": [0.25, 2]},
|
||||
"east": {"uv": [7, 15], "uv_size": [0.5, 2]},
|
||||
"south": {"uv": [16, 16], "uv_size": [0.25, 2]},
|
||||
"west": {"uv": [15, 10], "uv_size": [0.5, 2]},
|
||||
"up": {"uv": [12, 20], "uv_size": [0.25, 0.5]},
|
||||
"down": {"uv": [20, 12.5], "uv_size": [0.25, -0.5]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-2.48884, 7.3436, -3.07947],
|
||||
"size": [0.2878, 2.0128, 0.71905],
|
||||
"inflate": 0.001,
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [17, 1], "uv_size": [0.25, 2]},
|
||||
"east": {"uv": [14, 12], "uv_size": [0.75, 2]},
|
||||
"south": {"uv": [5, 17], "uv_size": [0.25, 2]},
|
||||
"west": {"uv": [14, 14], "uv_size": [0.75, 2]},
|
||||
"up": {"uv": [19, 13], "uv_size": [0.25, 0.75]},
|
||||
"down": {"uv": [14, 19.75], "uv_size": [0.25, -0.75]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-2.48869, 9.2125, -3.58244],
|
||||
"size": [0.2875, 0.71875, 1.725],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [19, 14], "uv_size": [0.25, 0.75]},
|
||||
"east": {"uv": [0, 15], "uv_size": [1.75, 0.75]},
|
||||
"south": {"uv": [15, 19], "uv_size": [0.25, 0.75]},
|
||||
"west": {"uv": [2, 15], "uv_size": [1.75, 0.75]},
|
||||
"up": {"uv": [17, 12], "uv_size": [0.25, 1.75]},
|
||||
"down": {"uv": [17, 15.75], "uv_size": [0.25, -1.75]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [0.8625, 9.93125, -0.51875],
|
||||
"size": [0.2875, 0.8625, 0.43125],
|
||||
"pivot": [0.8625, 9.93125, -0.51875],
|
||||
"rotation": [0, -90, -25],
|
||||
"uv": {
|
||||
"north": {"uv": [19, 15], "uv_size": [0.25, 0.75]},
|
||||
"east": {"uv": [17, 16], "uv_size": [0.5, 0.75]},
|
||||
"south": {"uv": [16, 19], "uv_size": [0.25, 0.75]},
|
||||
"west": {"uv": [17, 17], "uv_size": [0.5, 0.75]},
|
||||
"up": {"uv": [13, 20], "uv_size": [0.25, 0.5]},
|
||||
"down": {"uv": [20, 13.5], "uv_size": [0.25, -0.5]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-0.43125, 11.8, -0.56243],
|
||||
"size": [0.8625, 0.2875, 0.21563],
|
||||
"pivot": [0, 11.225, -1.09375],
|
||||
"rotation": [-90, 0, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [19, 16], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [21, 11], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [17, 19], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [12, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [19, 17], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [18, 19.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-2.48869, 9.93125, -3.15119],
|
||||
"size": [0.2875, 0.575, 0.8625],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [14, 20], "uv_size": [0.25, 0.5]},
|
||||
"east": {"uv": [0, 18], "uv_size": [0.75, 0.5]},
|
||||
"south": {"uv": [20, 14], "uv_size": [0.25, 0.5]},
|
||||
"west": {"uv": [1, 18], "uv_size": [0.75, 0.5]},
|
||||
"up": {"uv": [19, 18], "uv_size": [0.25, 0.75]},
|
||||
"down": {"uv": [19, 19.75], "uv_size": [0.25, -0.75]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-1.15, 9.93125, -0.51875],
|
||||
"size": [0.2875, 0.8625, 0.43125],
|
||||
"pivot": [-0.8625, 9.93125, -0.51875],
|
||||
"rotation": [0, 90, 25],
|
||||
"uv": {
|
||||
"north": {"uv": [0, 20], "uv_size": [0.25, 0.75]},
|
||||
"east": {"uv": [18, 1], "uv_size": [0.5, 0.75]},
|
||||
"south": {"uv": [20, 0], "uv_size": [0.25, 0.75]},
|
||||
"west": {"uv": [2, 18], "uv_size": [0.5, 0.75]},
|
||||
"up": {"uv": [15, 20], "uv_size": [0.25, 0.5]},
|
||||
"down": {"uv": [20, 15.5], "uv_size": [0.25, -0.5]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-9.39461, 1.45, -5.73743],
|
||||
"size": [0.8625, 0.2875, 0.21563],
|
||||
"pivot": [0, 0.875, -1.09375],
|
||||
"rotation": [-90, 0, 60],
|
||||
"uv": {
|
||||
"north": {"uv": [1, 20], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [21, 12], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [20, 1], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [13, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [2, 20], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [20, 2.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [8.53211, 1.45, -5.73743],
|
||||
"size": [0.8625, 0.2875, 0.21563],
|
||||
"pivot": [0, 0.875, -1.09375],
|
||||
"rotation": [-90, 0, -60],
|
||||
"uv": {
|
||||
"north": {"uv": [3, 20], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [21, 13], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [20, 3], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [14, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [4, 20], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [20, 4.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [8.53211, 1.45, 4.61257],
|
||||
"size": [0.8625, 0.2875, 0.21563],
|
||||
"pivot": [0, 0.875, -1.09375],
|
||||
"rotation": [-90, 0, -120],
|
||||
"uv": {
|
||||
"north": {"uv": [5, 20], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [21, 14], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [20, 5], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [15, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [6, 20], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [20, 6.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-0.43125, 1.45, 9.78757],
|
||||
"size": [0.8625, 0.2875, 0.21563],
|
||||
"pivot": [0, 0.875, -1.09375],
|
||||
"rotation": [-90, 0, 180],
|
||||
"uv": {
|
||||
"north": {"uv": [7, 20], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [21, 15], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [20, 7], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [16, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [8, 20], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [20, 8.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-9.39461, 1.45, 4.61257],
|
||||
"size": [0.8625, 0.2875, 0.21563],
|
||||
"pivot": [0, 0.875, -1.09375],
|
||||
"rotation": [-90, 0, 120],
|
||||
"uv": {
|
||||
"north": {"uv": [9, 20], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [21, 16], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [20, 9], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [17, 21], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [10, 20], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [20, 10.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "righthand",
|
||||
"parent": "knife_and_right",
|
||||
"pivot": [6, 19, 0]
|
||||
},
|
||||
{
|
||||
"name": "righthand_pos",
|
||||
"parent": "righthand",
|
||||
"pivot": [0, 8, 0],
|
||||
"cubes": [
|
||||
{
|
||||
"origin": [4, 8, -2],
|
||||
"size": [4, 12, 4],
|
||||
"uv": {
|
||||
"north": {"uv": [0, 0], "uv_size": [4, 12]},
|
||||
"east": {"uv": [4, 0], "uv_size": [4, 12]},
|
||||
"south": {"uv": [8, 0], "uv_size": [4, 12]},
|
||||
"west": {"uv": [0, 12], "uv_size": [4, 12]},
|
||||
"up": {"uv": [16, 0], "uv_size": [4, 4]},
|
||||
"down": {"uv": [16, 8], "uv_size": [4, -4]}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "constraint",
|
||||
"parent": "bone",
|
||||
"pivot": [-0.01558, 8.125, 1.9375]
|
||||
},
|
||||
{
|
||||
"name": "lefthand",
|
||||
"parent": "bone",
|
||||
"pivot": [-6, 19, 0]
|
||||
},
|
||||
{
|
||||
"name": "lefthand_pos",
|
||||
"parent": "lefthand",
|
||||
"pivot": [0, 8, 0],
|
||||
"cubes": [
|
||||
{
|
||||
"origin": [-8, 8, -2],
|
||||
"size": [4, 12, 4],
|
||||
"uv": {
|
||||
"north": {"uv": [12.228, 0], "uv_size": [3.614, 12]},
|
||||
"east": {"uv": [5, 12], "uv_size": [3.614, 12]},
|
||||
"south": {"uv": [8.614, 12], "uv_size": [3.614, 12]},
|
||||
"west": {"uv": [12.228, 12], "uv_size": [3.614, 12]},
|
||||
"up": {"uv": [15.842, 8], "uv_size": [3.614, 4]},
|
||||
"down": {"uv": [15.842, 16], "uv_size": [3.614, -4]}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "camera",
|
||||
"pivot": [3.0813, 14.0031, 11.5313]
|
||||
},
|
||||
{
|
||||
"name": "view",
|
||||
"pivot": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"name": "refit_view",
|
||||
"parent": "view",
|
||||
"pivot": [22.475, 5.55, 0.2],
|
||||
"rotation": [0, 90, 0]
|
||||
},
|
||||
{
|
||||
"name": "iron_view",
|
||||
"parent": "view",
|
||||
"pivot": [-0.15326, 9.42188, 6.93493],
|
||||
"rotation": [0, -1, 0]
|
||||
},
|
||||
{
|
||||
"name": "idle_view",
|
||||
"parent": "view",
|
||||
"pivot": [3.4563, 4.4656, -0.7687]
|
||||
},
|
||||
{
|
||||
"name": "positioning",
|
||||
"pivot": [-0.01558, 4.125, 4.9375]
|
||||
},
|
||||
{
|
||||
"name": "ground",
|
||||
"parent": "positioning",
|
||||
"pivot": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"name": "thirdperson_hand",
|
||||
"parent": "positioning",
|
||||
"pivot": [-0.01558, 6.125, -2.0625]
|
||||
},
|
||||
{
|
||||
"name": "fixed",
|
||||
"parent": "positioning",
|
||||
"pivot": [0.79692, 5.125, -1.0625],
|
||||
"rotation": [0, 90, 0]
|
||||
},
|
||||
{
|
||||
"name": "bone38",
|
||||
"pivot": [62, -11.35, -167.5]
|
||||
},
|
||||
{
|
||||
"name": "bone39",
|
||||
"parent": "bone38",
|
||||
"pivot": [-65, -11.35, -167.5]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
3449
mod/src/main/resources/assets/lovisual/knife/butterfly.geo.json
Normal file
3449
mod/src/main/resources/assets/lovisual/knife/butterfly.geo.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1639
mod/src/main/resources/assets/lovisual/knife/css.geo.json
Normal file
1639
mod/src/main/resources/assets/lovisual/knife/css.geo.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
mod/src/main/resources/assets/lovisual/knife/index.json
Normal file
1
mod/src/main/resources/assets/lovisual/knife/index.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3153
mod/src/main/resources/assets/lovisual/knife/karambit.geo.json
Normal file
3153
mod/src/main/resources/assets/lovisual/knife/karambit.geo.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
741
mod/src/main/resources/assets/lovisual/knife/m9.geo.json
Normal file
741
mod/src/main/resources/assets/lovisual/knife/m9.geo.json
Normal file
|
|
@ -0,0 +1,741 @@
|
|||
{
|
||||
"format_version": "1.12.0",
|
||||
"minecraft:geometry": [
|
||||
{
|
||||
"description": {
|
||||
"identifier": "geometry.unknown",
|
||||
"texture_width": 128,
|
||||
"texture_height": 128,
|
||||
"visible_bounds_width": 4,
|
||||
"visible_bounds_height": 4,
|
||||
"visible_bounds_offset": [0, 0, 0]
|
||||
},
|
||||
"bones": [
|
||||
{
|
||||
"name": "root",
|
||||
"pivot": [0, 9.6875, 4.3375]
|
||||
},
|
||||
{
|
||||
"name": "bone",
|
||||
"parent": "root",
|
||||
"pivot": [0, 9.6875, 4.3375]
|
||||
},
|
||||
{
|
||||
"name": "knife_and_right",
|
||||
"parent": "bone",
|
||||
"pivot": [6, 19, 0]
|
||||
},
|
||||
{
|
||||
"name": "m9",
|
||||
"parent": "knife_and_right",
|
||||
"pivot": [0, 9.5, 2.5]
|
||||
},
|
||||
{
|
||||
"name": "weapon",
|
||||
"parent": "m9",
|
||||
"pivot": [0, 9.5, -0.375]
|
||||
},
|
||||
{
|
||||
"name": "blade",
|
||||
"parent": "weapon",
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"cubes": [
|
||||
{
|
||||
"origin": [-0.149, 10.235, -5.6925],
|
||||
"size": [0.297, 0.056, 3.74],
|
||||
"uv": {
|
||||
"north": {"uv": [0, 0], "uv_size": [2, 2]},
|
||||
"east": {"uv": [7.5, 34.5], "uv_size": [2, 2]},
|
||||
"south": {"uv": [0, 0], "uv_size": [2, 2]},
|
||||
"west": {"uv": [10.25, 34.5], "uv_size": [2, 2]},
|
||||
"up": {"uv": [17.5, 36.5], "uv_size": [-2, -2]},
|
||||
"down": {"uv": [2, 2], "uv_size": [-2, -2]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-2.20119, 8.6375, -2.86369],
|
||||
"size": [1.4375, 1.86875, 0.2875],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [0, 10], "uv_size": [1.25, 1.75]},
|
||||
"east": {"uv": [2, 22], "uv_size": [0.25, 1.75]},
|
||||
"south": {"uv": [3, 10], "uv_size": [1.25, 1.75]},
|
||||
"west": {"uv": [8, 22], "uv_size": [0.25, 1.75]},
|
||||
"up": {"uv": [22, 23], "uv_size": [1.25, 0.25]},
|
||||
"down": {"uv": [24, 6.25], "uv_size": [1.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-1.58658, 8.6375, -3.68125],
|
||||
"size": [3.7375, 1.6, 0.29817],
|
||||
"pivot": [-0.00533, 9.46406, -3.5375],
|
||||
"rotation": [0, 90, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [6, 2], "uv_size": [3.25, 1.5]},
|
||||
"east": {"uv": [23, 11], "uv_size": [0.25, 1.5]},
|
||||
"south": {"uv": [7, 5], "uv_size": [3.25, 1.5]},
|
||||
"west": {"uv": [14, 23], "uv_size": [0.25, 1.5]},
|
||||
"up": {"uv": [19, 6], "uv_size": [3.25, 0.25]},
|
||||
"down": {"uv": [14, 19.25], "uv_size": [3.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [5.81387, 7.6926, -2.86369],
|
||||
"size": [0.8625, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [-90, 80, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [12, 25], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [6, 30], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [25, 12], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [30, 8], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [25, 14], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [16, 25.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [6.16855, 6.56882, -2.86369],
|
||||
"size": [0.73671, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [-90, 70, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [25, 16], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [14, 30], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [18, 25], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [30, 20], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [25, 18], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [20, 25.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [5.77341, 4.89964, -2.86369],
|
||||
"size": [0.83554, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [-90, 55, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [22, 25], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [30, 24], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [25, 22], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [26, 30], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [24, 25], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [4, 26.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-1.33869, 8.94455, -3.80829],
|
||||
"size": [6.325, 0.2033, 0.2033],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [13, 0], "uv_size": [5.5, 0.25]},
|
||||
"east": {"uv": [30, 26], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [3, 13], "uv_size": [5.5, 0.25]},
|
||||
"west": {"uv": [28, 30], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [10, 13], "uv_size": [5.5, 0.25]},
|
||||
"down": {"uv": [14, 2.25], "uv_size": [5.5, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [5.81387, 8.27641, -4.47643],
|
||||
"size": [0.8625, 0.2033, 0.2033],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [-45, 80, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [6, 26], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [30, 28], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [26, 8], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [30, 30], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [14, 26], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [26, 20.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [6.16855, 7.48178, -5.27107],
|
||||
"size": [0.73671, 0.20329, 0.2033],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [-45, 70, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [26, 24], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [0, 31], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [26, 26], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [31, 0], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [0, 27], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [27, 0.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [5.71387, 6.33693, 0.77401],
|
||||
"size": [0.83554, 0.2033, 0.2033],
|
||||
"pivot": [-0.05955, 9.98413, -2.7541],
|
||||
"rotation": [-135, 55, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [2, 27], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [2, 31], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [27, 2], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [31, 2], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [27, 4], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [27, 6.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [5.80726, 10.7815, -8.72829],
|
||||
"size": [0.20329, 0.21562, 0.2033],
|
||||
"pivot": [0, 10.1649, -2.71773],
|
||||
"rotation": [11.2413, 44.4451, 7.9231],
|
||||
"uv": {
|
||||
"north": {"uv": [31, 12], "uv_size": [0.25, 0.25]},
|
||||
"east": {"uv": [31, 14], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [16, 31], "uv_size": [0.25, 0.25]},
|
||||
"west": {"uv": [31, 16], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [18, 31], "uv_size": [0.25, 0.25]},
|
||||
"down": {"uv": [31, 18.25], "uv_size": [0.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [7.99708, 10.78151, -2.86148],
|
||||
"size": [0.35938, 0.21562, 0.2875],
|
||||
"pivot": [0, 10.1649, -2.71773],
|
||||
"rotation": [90, 82, 90],
|
||||
"uv": {
|
||||
"north": {"uv": [31, 4], "uv_size": [0.25, 0.25]},
|
||||
"east": {"uv": [31, 6], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [8, 31], "uv_size": [0.25, 0.25]},
|
||||
"west": {"uv": [10, 31], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [31, 10], "uv_size": [0.25, 0.25]},
|
||||
"down": {"uv": [12, 31.25], "uv_size": [0.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [0.14375, 10.14688, -6.55625],
|
||||
"size": [4.52813, 0.35938, 0.2875],
|
||||
"pivot": [0.14375, 10.50625, -6.26875],
|
||||
"rotation": [90, 82, 90],
|
||||
"uv": {
|
||||
"north": {"uv": [15, 10], "uv_size": [4, 0.25]},
|
||||
"east": {"uv": [24, 29], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [11, 15], "uv_size": [4, 0.25]},
|
||||
"west": {"uv": [4, 30], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [16, 15], "uv_size": [4, 0.25]},
|
||||
"down": {"uv": [17, 4.25], "uv_size": [4, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [2.97366, 8.63735, -2.86384],
|
||||
"size": [5.4628, 1.86905, 0.2878],
|
||||
"inflate": 0.001,
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [4.75, 0], "uv_size": [-4.75, 1.75]},
|
||||
"east": {"uv": [0, 22], "uv_size": [0.25, 1.75]},
|
||||
"south": {"uv": [4.75, 3], "uv_size": [-4.75, 1.75]},
|
||||
"up": {"uv": [18.75, 8], "uv_size": [-4.75, 0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-0.67144, 9.72594, -2.93557],
|
||||
"size": [0.2875, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -2.79182],
|
||||
"rotation": [90, 45, 90],
|
||||
"uv": {
|
||||
"north": {"uv": [20, 31], "uv_size": [0.25, 0.25]},
|
||||
"east": {"uv": [22, 31], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [31, 22], "uv_size": [0.25, 0.25]},
|
||||
"west": {"uv": [24, 31], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [4, 32], "uv_size": [0.25, 0.25]},
|
||||
"down": {"uv": [6, 32.25], "uv_size": [0.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-0.31567, 10.0817, -2.93557],
|
||||
"size": [0.2875, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -2.79182],
|
||||
"rotation": [90, 45, 90],
|
||||
"uv": {
|
||||
"north": {"uv": [32, 8], "uv_size": [0.25, 0.25]},
|
||||
"east": {"uv": [14, 32], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [32, 20], "uv_size": [0.25, 0.25]},
|
||||
"west": {"uv": [32, 24], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [26, 32], "uv_size": [0.25, 0.25]},
|
||||
"down": {"uv": [32, 26.25], "uv_size": [0.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [0.04009, 10.43747, -2.93557],
|
||||
"size": [0.2875, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -2.79182],
|
||||
"rotation": [90, 45, 90],
|
||||
"uv": {
|
||||
"north": {"uv": [28, 32], "uv_size": [0.25, 0.25]},
|
||||
"east": {"uv": [32, 28], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [30, 32], "uv_size": [0.25, 0.25]},
|
||||
"west": {"uv": [32, 30], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [32, 32], "uv_size": [0.25, 0.25]},
|
||||
"down": {"uv": [0, 33.25], "uv_size": [0.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [0.39585, 10.79323, -2.93557],
|
||||
"size": [0.2875, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -2.79182],
|
||||
"rotation": [90, 45, 90],
|
||||
"uv": {
|
||||
"north": {"uv": [33, 0], "uv_size": [0.25, 0.25]},
|
||||
"east": {"uv": [2, 33], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [33, 2], "uv_size": [0.25, 0.25]},
|
||||
"west": {"uv": [33, 4], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [33, 6], "uv_size": [0.25, 0.25]},
|
||||
"down": {"uv": [8, 33.25], "uv_size": [0.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [0.75162, 11.149, -2.93557],
|
||||
"size": [0.2875, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -2.79182],
|
||||
"rotation": [90, 45, 90],
|
||||
"uv": {
|
||||
"north": {"uv": [10, 33], "uv_size": [0.25, 0.25]},
|
||||
"east": {"uv": [33, 10], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [12, 33], "uv_size": [0.25, 0.25]},
|
||||
"west": {"uv": [33, 12], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [33, 14], "uv_size": [0.25, 0.25]},
|
||||
"down": {"uv": [16, 33.25], "uv_size": [0.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [1.10738, 11.50476, -2.93557],
|
||||
"size": [0.2875, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -2.79182],
|
||||
"rotation": [90, 45, 90],
|
||||
"uv": {
|
||||
"north": {"uv": [33, 16], "uv_size": [0.25, 0.25]},
|
||||
"east": {"uv": [18, 33], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [33, 18], "uv_size": [0.25, 0.25]},
|
||||
"west": {"uv": [20, 33], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [22, 33], "uv_size": [0.25, 0.25]},
|
||||
"down": {"uv": [33, 22.25], "uv_size": [0.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [0.75162, 11.149, -3.94182],
|
||||
"size": [0.2875, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -3.79807],
|
||||
"rotation": [90, 45, 90],
|
||||
"uv": {
|
||||
"north": {"uv": [0, 35], "uv_size": [0.25, 0.25]},
|
||||
"east": {"uv": [35, 0], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [2, 35], "uv_size": [0.25, 0.25]},
|
||||
"west": {"uv": [35, 2], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [35, 4], "uv_size": [0.25, 0.25]},
|
||||
"down": {"uv": [35, 6.25], "uv_size": [0.25, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [4.84571, 8.64101, -4.11183],
|
||||
"size": [1.15, 0.2033, 0.2033],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [-45, 85, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [24, 8], "uv_size": [1, 0.25]},
|
||||
"east": {"uv": [24, 33], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [24, 20], "uv_size": [1, 0.25]},
|
||||
"west": {"uv": [4, 34], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [0, 25], "uv_size": [1, 0.25]},
|
||||
"down": {"uv": [25, 0.25], "uv_size": [1, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [4.84571, 8.20823, -2.86369],
|
||||
"size": [1.15, 0.2875, 0.2875],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [-90, 85, -90],
|
||||
"uv": {
|
||||
"north": {"uv": [2, 25], "uv_size": [1, 0.25]},
|
||||
"east": {"uv": [6, 34], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [25, 2], "uv_size": [1, 0.25]},
|
||||
"west": {"uv": [34, 8], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [25, 4], "uv_size": [1, 0.25]},
|
||||
"down": {"uv": [8, 25.25], "uv_size": [1, -0.25]}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "grip",
|
||||
"parent": "weapon",
|
||||
"pivot": [0, 9.49999, 3.22902],
|
||||
"cubes": [
|
||||
{
|
||||
"origin": [-9.24494, 9.15295, -3.59989],
|
||||
"size": [6.75625, 1.00625, 1.00625],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [0, 6], "uv_size": [6, 1]},
|
||||
"east": {"uv": [19, 17], "uv_size": [1, 1]},
|
||||
"south": {"uv": [6, 0], "uv_size": [6, 1]},
|
||||
"west": {"uv": [19, 19], "uv_size": [0.75, 1]},
|
||||
"up": {"uv": [0, 8], "uv_size": [6, 0.75]},
|
||||
"down": {"uv": [7, 8.75], "uv_size": [6, -0.75]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-3.63869, 9.08108, -3.67176],
|
||||
"size": [1.15, 1.15, 1.15],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [0, 16], "uv_size": [1, 1]},
|
||||
"east": {"uv": [5, 17], "uv_size": [1, 1]},
|
||||
"south": {"uv": [17, 6], "uv_size": [1, 1]},
|
||||
"west": {"uv": [7, 17], "uv_size": [1, 1]},
|
||||
"up": {"uv": [9, 17], "uv_size": [1, 1]},
|
||||
"down": {"uv": [11, 18], "uv_size": [1, -1]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-4.78869, 9.08108, -3.67176],
|
||||
"size": [0.8625, 1.15, 1.15],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [0, 20], "uv_size": [0.75, 1]},
|
||||
"east": {"uv": [17, 12], "uv_size": [1, 1]},
|
||||
"south": {"uv": [20, 0], "uv_size": [0.75, 1]},
|
||||
"west": {"uv": [13, 17], "uv_size": [1, 1]},
|
||||
"up": {"uv": [2, 20], "uv_size": [0.75, 1]},
|
||||
"down": {"uv": [20, 9], "uv_size": [0.75, -1]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-5.93869, 9.08108, -3.67176],
|
||||
"size": [0.8625, 1.15, 1.15],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [20, 10], "uv_size": [0.75, 1]},
|
||||
"east": {"uv": [15, 17], "uv_size": [1, 1]},
|
||||
"south": {"uv": [21, 2], "uv_size": [0.75, 1]},
|
||||
"west": {"uv": [17, 17], "uv_size": [1, 1]},
|
||||
"up": {"uv": [4, 21], "uv_size": [0.75, 1]},
|
||||
"down": {"uv": [6, 22], "uv_size": [0.75, -1]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-7.08869, 9.08108, -3.67176],
|
||||
"size": [0.8625, 1.15, 1.15],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [21, 12], "uv_size": [0.75, 1]},
|
||||
"east": {"uv": [0, 18], "uv_size": [1, 1]},
|
||||
"south": {"uv": [14, 21], "uv_size": [0.75, 1]},
|
||||
"west": {"uv": [2, 18], "uv_size": [1, 1]},
|
||||
"up": {"uv": [21, 14], "uv_size": [0.75, 1]},
|
||||
"down": {"uv": [16, 22], "uv_size": [0.75, -1]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-9.10119, 8.93733, -3.81551],
|
||||
"size": [1.4375, 1.4375, 1.4375],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [6, 10], "uv_size": [1.25, 1.25]},
|
||||
"east": {"uv": [9, 10], "uv_size": [1.25, 1.25]},
|
||||
"south": {"uv": [11, 2], "uv_size": [1.25, 1.25]},
|
||||
"west": {"uv": [12, 5], "uv_size": [1.25, 1.25]},
|
||||
"up": {"uv": [12, 10], "uv_size": [1.25, 1.25]},
|
||||
"down": {"uv": [0, 14.25], "uv_size": [1.25, -1.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-8.23869, 9.08108, -3.67176],
|
||||
"size": [0.8625, 1.15, 1.15],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, -45],
|
||||
"uv": {
|
||||
"north": {"uv": [21, 16], "uv_size": [0.75, 1]},
|
||||
"east": {"uv": [4, 19], "uv_size": [1, 1]},
|
||||
"south": {"uv": [18, 21], "uv_size": [0.75, 1]},
|
||||
"west": {"uv": [6, 19], "uv_size": [1, 1]},
|
||||
"up": {"uv": [21, 18], "uv_size": [0.75, 1]},
|
||||
"down": {"uv": [20, 22], "uv_size": [0.75, -1]}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bone3",
|
||||
"parent": "weapon",
|
||||
"pivot": [0, 0.875, -1.09375],
|
||||
"cubes": [
|
||||
{
|
||||
"origin": [0.8625, 7.27188, -0.80625],
|
||||
"size": [0.2875, 1.94063, 0.575],
|
||||
"pivot": [0.8625, 9.2125, -0.23125],
|
||||
"rotation": [0, 90, 15],
|
||||
"uv": {
|
||||
"north": {"uv": [12, 22], "uv_size": [0.25, 1.75]},
|
||||
"east": {"uv": [8, 19], "uv_size": [0.5, 1.75]},
|
||||
"south": {"uv": [22, 20], "uv_size": [0.25, 1.75]},
|
||||
"west": {"uv": [10, 19], "uv_size": [0.5, 1.75]},
|
||||
"up": {"uv": [29, 18], "uv_size": [0.25, 0.5]},
|
||||
"down": {"uv": [20, 29.5], "uv_size": [0.25, -0.5]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-0.8625, 7.27188, -1.09375],
|
||||
"size": [0.2875, 1.94063, 0.575],
|
||||
"pivot": [-0.8625, 9.2125, -0.51875],
|
||||
"rotation": [0, -90, -15],
|
||||
"uv": {
|
||||
"north": {"uv": [23, 0], "uv_size": [0.25, 1.75]},
|
||||
"east": {"uv": [12, 19], "uv_size": [0.5, 1.75]},
|
||||
"south": {"uv": [23, 3], "uv_size": [0.25, 1.75]},
|
||||
"west": {"uv": [19, 12], "uv_size": [0.5, 1.75]},
|
||||
"up": {"uv": [22, 29], "uv_size": [0.25, 0.5]},
|
||||
"down": {"uv": [29, 22.5], "uv_size": [0.25, -0.5]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-2.48884, 7.3436, -3.07947],
|
||||
"size": [0.2878, 2.0128, 0.71905],
|
||||
"inflate": 0.001,
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [22, 8], "uv_size": [0.25, 1.75]},
|
||||
"east": {"uv": [3, 15], "uv_size": [0.75, 1.75]},
|
||||
"south": {"uv": [10, 22], "uv_size": [0.25, 1.75]},
|
||||
"west": {"uv": [15, 4], "uv_size": [0.75, 1.75]},
|
||||
"up": {"uv": [10, 29], "uv_size": [0.25, 0.75]},
|
||||
"down": {"uv": [29, 10.75], "uv_size": [0.25, -0.75]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-2.48869, 9.2125, -3.58244],
|
||||
"size": [0.2875, 0.71875, 1.725],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [10, 25], "uv_size": [0.25, 0.75]},
|
||||
"east": {"uv": [5, 15], "uv_size": [1.5, 0.75]},
|
||||
"south": {"uv": [25, 10], "uv_size": [0.25, 0.75]},
|
||||
"west": {"uv": [8, 15], "uv_size": [1.5, 0.75]},
|
||||
"up": {"uv": [4, 23], "uv_size": [0.25, 1.5]},
|
||||
"down": {"uv": [6, 24.5], "uv_size": [0.25, -1.5]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [0.8625, 9.93125, -0.51875],
|
||||
"size": [0.2875, 0.8625, 0.43125],
|
||||
"pivot": [0.8625, 9.93125, -0.51875],
|
||||
"rotation": [0, -90, -25],
|
||||
"uv": {
|
||||
"north": {"uv": [29, 6], "uv_size": [0.25, 0.75]},
|
||||
"east": {"uv": [23, 18], "uv_size": [0.5, 0.75]},
|
||||
"south": {"uv": [8, 29], "uv_size": [0.25, 0.75]},
|
||||
"west": {"uv": [20, 23], "uv_size": [0.5, 0.75]},
|
||||
"up": {"uv": [29, 16], "uv_size": [0.25, 0.5]},
|
||||
"down": {"uv": [18, 29.5], "uv_size": [0.25, -0.5]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-0.43125, 11.8, -0.56243],
|
||||
"size": [0.8625, 0.2875, 0.21563],
|
||||
"pivot": [0, 11.225, -1.09375],
|
||||
"rotation": [-90, 0, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [8, 27], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [14, 34], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [10, 27], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [34, 20], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [27, 10], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [12, 27.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-2.48869, 9.93125, -3.15119],
|
||||
"size": [0.2875, 0.575, 0.8625],
|
||||
"pivot": [0, 10.0329, -2.71994],
|
||||
"rotation": [0, 90, 0],
|
||||
"uv": {
|
||||
"north": {"uv": [12, 29], "uv_size": [0.25, 0.5]},
|
||||
"east": {"uv": [23, 14], "uv_size": [0.75, 0.5]},
|
||||
"south": {"uv": [29, 12], "uv_size": [0.25, 0.5]},
|
||||
"west": {"uv": [16, 23], "uv_size": [0.75, 0.5]},
|
||||
"up": {"uv": [29, 0], "uv_size": [0.25, 0.75]},
|
||||
"down": {"uv": [2, 29.75], "uv_size": [0.25, -0.75]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-1.15, 9.93125, -0.51875],
|
||||
"size": [0.2875, 0.8625, 0.43125],
|
||||
"pivot": [-0.8625, 9.93125, -0.51875],
|
||||
"rotation": [0, 90, 25],
|
||||
"uv": {
|
||||
"north": {"uv": [29, 2], "uv_size": [0.25, 0.75]},
|
||||
"east": {"uv": [23, 16], "uv_size": [0.5, 0.75]},
|
||||
"south": {"uv": [29, 4], "uv_size": [0.25, 0.75]},
|
||||
"west": {"uv": [18, 23], "uv_size": [0.5, 0.75]},
|
||||
"up": {"uv": [29, 14], "uv_size": [0.25, 0.5]},
|
||||
"down": {"uv": [16, 29.5], "uv_size": [0.25, -0.5]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-9.39461, 1.45, -5.73743],
|
||||
"size": [0.8625, 0.2875, 0.21563],
|
||||
"pivot": [0, 0.875, -1.09375],
|
||||
"rotation": [-90, 0, 60],
|
||||
"uv": {
|
||||
"north": {"uv": [27, 12], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [34, 24], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [27, 14], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [26, 34], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [16, 27], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [27, 16.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [8.53211, 1.45, -5.73743],
|
||||
"size": [0.8625, 0.2875, 0.21563],
|
||||
"pivot": [0, 0.875, -1.09375],
|
||||
"rotation": [-90, 0, -60],
|
||||
"uv": {
|
||||
"north": {"uv": [26, 28], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [34, 32], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [28, 26], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [34, 34], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [28, 28], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [0, 29.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [8.53211, 1.45, 4.61257],
|
||||
"size": [0.8625, 0.2875, 0.21563],
|
||||
"pivot": [0, 0.875, -1.09375],
|
||||
"rotation": [-90, 0, -120],
|
||||
"uv": {
|
||||
"north": {"uv": [28, 8], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [34, 30], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [14, 28], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [32, 34], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [28, 20], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [28, 24.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-0.43125, 1.45, 9.78757],
|
||||
"size": [0.8625, 0.2875, 0.21563],
|
||||
"pivot": [0, 0.875, -1.09375],
|
||||
"rotation": [-90, 0, 180],
|
||||
"uv": {
|
||||
"north": {"uv": [27, 22], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [34, 28], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [24, 27], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [30, 34], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [4, 28], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [6, 28.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
},
|
||||
{
|
||||
"origin": [-9.39461, 1.45, 4.61257],
|
||||
"size": [0.8625, 0.2875, 0.21563],
|
||||
"pivot": [0, 0.875, -1.09375],
|
||||
"rotation": [-90, 0, 120],
|
||||
"uv": {
|
||||
"north": {"uv": [18, 27], "uv_size": [0.75, 0.25]},
|
||||
"east": {"uv": [34, 26], "uv_size": [0.25, 0.25]},
|
||||
"south": {"uv": [27, 18], "uv_size": [0.75, 0.25]},
|
||||
"west": {"uv": [28, 34], "uv_size": [0.25, 0.25]},
|
||||
"up": {"uv": [20, 27], "uv_size": [0.75, 0.25]},
|
||||
"down": {"uv": [22, 27.25], "uv_size": [0.75, -0.25]}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "righthand",
|
||||
"parent": "knife_and_right",
|
||||
"pivot": [6, 19, 0]
|
||||
},
|
||||
{
|
||||
"name": "righthand_pos",
|
||||
"parent": "righthand",
|
||||
"pivot": [0, 8, 0]
|
||||
},
|
||||
{
|
||||
"name": "constraint",
|
||||
"parent": "bone",
|
||||
"pivot": [-0.01558, 8.125, 1.9375]
|
||||
},
|
||||
{
|
||||
"name": "lefthand",
|
||||
"parent": "bone",
|
||||
"pivot": [-6, 19, 0]
|
||||
},
|
||||
{
|
||||
"name": "lefthand_pos",
|
||||
"parent": "lefthand",
|
||||
"pivot": [0, 8, 0]
|
||||
},
|
||||
{
|
||||
"name": "camera",
|
||||
"pivot": [3.0813, 14.0031, 11.5313]
|
||||
},
|
||||
{
|
||||
"name": "view",
|
||||
"pivot": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"name": "refit_view",
|
||||
"parent": "view",
|
||||
"pivot": [22.475, 5.55, 0.2],
|
||||
"rotation": [0, 90, 0]
|
||||
},
|
||||
{
|
||||
"name": "iron_view",
|
||||
"parent": "view",
|
||||
"pivot": [-0.15326, 9.42188, 6.93493],
|
||||
"rotation": [0, -1, 0]
|
||||
},
|
||||
{
|
||||
"name": "idle_view",
|
||||
"parent": "view",
|
||||
"pivot": [3.4563, 2.36563, 0.53125]
|
||||
},
|
||||
{
|
||||
"name": "positioning",
|
||||
"pivot": [-0.01558, 4.125, 4.9375]
|
||||
},
|
||||
{
|
||||
"name": "ground",
|
||||
"parent": "positioning",
|
||||
"pivot": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"name": "thirdperson_hand",
|
||||
"parent": "positioning",
|
||||
"pivot": [-0.01558, 6.125, -2.0625]
|
||||
},
|
||||
{
|
||||
"name": "fixed",
|
||||
"parent": "positioning",
|
||||
"pivot": [0.79692, 5.125, -1.0625],
|
||||
"rotation": [0, 90, 0]
|
||||
},
|
||||
{
|
||||
"name": "bone38",
|
||||
"pivot": [62, -11.35, -167.5]
|
||||
},
|
||||
{
|
||||
"name": "bone39",
|
||||
"parent": "bone38",
|
||||
"pivot": [-65, -11.35, -167.5]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5377
mod/src/main/resources/assets/lovisual/knife/push.geo.json
Normal file
5377
mod/src/main/resources/assets/lovisual/knife/push.geo.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2481
mod/src/main/resources/assets/lovisual/knife/skeleton.geo.json
Normal file
2481
mod/src/main/resources/assets/lovisual/knife/skeleton.geo.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
4765
mod/src/main/resources/assets/lovisual/knife/stiletto.geo.json
Normal file
4765
mod/src/main/resources/assets/lovisual/knife/stiletto.geo.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
2294
mod/src/main/resources/assets/lovisual/knife/tactical.geo.json
Normal file
2294
mod/src/main/resources/assets/lovisual/knife/tactical.geo.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
5101
mod/src/main/resources/assets/lovisual/knife/talon.geo.json
Normal file
5101
mod/src/main/resources/assets/lovisual/knife/talon.geo.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue