diff --git a/src/main/java/com/wildfire/physics/BreastPhysics.java b/src/main/java/com/wildfire/physics/BreastPhysics.java index 51007f58..76a0a855 100644 --- a/src/main/java/com/wildfire/physics/BreastPhysics.java +++ b/src/main/java/com/wildfire/physics/BreastPhysics.java @@ -53,6 +53,8 @@ public class BreastPhysics { private float breastSize = 0, preBreastSize = 0; + private float preVelocity; + private @Nullable Pose lastPose; private int lastSwingDuration = 6, lastSwingTick = 0; private @Nullable Vec3 prePos; @@ -121,6 +123,7 @@ public void update(LivingEntity entity, IGenderArmor armor) { this.prePositionX = this.positionX; this.wfg_preBounceRotation = this.wfg_bounceRotation; this.preBreastSize = this.breastSize; + this.preVelocity = this.velocity; if(this.prePos == null) { this.prePos = entity.position(); @@ -373,6 +376,13 @@ public float getPreBreastSize() { return this.preBreastSize; } + public float getVelocity() { + return this.velocity; + } + public float getPreVelocity() { + return this.preVelocity; + } + private int clampMovement(float movement) { return Math.max((int) (10 - movement*2f), 1); } diff --git a/src/main/java/com/wildfire/render/BreastRenderCommand.java b/src/main/java/com/wildfire/render/BreastRenderCommand.java index a668eedb..84478372 100644 --- a/src/main/java/com/wildfire/render/BreastRenderCommand.java +++ b/src/main/java/com/wildfire/render/BreastRenderCommand.java @@ -37,14 +37,19 @@ public record BreastRenderCommand( int overlay, int color, int outline, - @Nullable UnaryOperator consumerOperator + @Nullable UnaryOperator consumerOperator, + PhysicsDeformation physics ) implements SubmitNodeCollector.CustomGeometryRenderer { public BreastRenderCommand(WildfireModelRenderer.ModelBox model, LivingEntityRenderState state, int overlay, int color) { - this(model, state.lightCoords, overlay, color, state.outlineColor, null); + this(model, state.lightCoords, overlay, color, state.outlineColor, null, PhysicsDeformation.NONE); } - public static BreastRenderCommand trim(WildfireModelRenderer.ModelBox model, LivingEntityRenderState state, TextureAtlasSprite trimSprite) { - return new BreastRenderCommand(model, state.lightCoords, OverlayTexture.NO_OVERLAY, -1, 0, trimSprite::wrap); + public BreastRenderCommand(WildfireModelRenderer.ModelBox model, LivingEntityRenderState state, int overlay, int color, PhysicsDeformation physics) { + this(model, state.lightCoords, overlay, color, state.outlineColor, null, physics); + } + + public static BreastRenderCommand trim(WildfireModelRenderer.ModelBox model, LivingEntityRenderState state, TextureAtlasSprite trimSprite, PhysicsDeformation physics) { + return new BreastRenderCommand(model, state.lightCoords, OverlayTexture.NO_OVERLAY, -1, 0, trimSprite::wrap, physics); } @Override @@ -52,6 +57,6 @@ public void render(PoseStack.Pose matricesEntry, VertexConsumer vertexConsumer) if(consumerOperator != null) { vertexConsumer = consumerOperator.apply(vertexConsumer); } - GenderLayer.renderBox(model, matricesEntry, vertexConsumer, light, overlay, color); + GenderLayer.renderBox(model, matricesEntry, vertexConsumer, light, overlay, color, physics); } } diff --git a/src/main/java/com/wildfire/render/GenderArmorLayer.java b/src/main/java/com/wildfire/render/GenderArmorLayer.java index f7d04382..5f4b16c7 100644 --- a/src/main/java/com/wildfire/render/GenderArmorLayer.java +++ b/src/main/java/com/wildfire/render/GenderArmorLayer.java @@ -191,12 +191,14 @@ protected void renderBreastArmor(Identifier texture, PoseStack matrixStack, Subm return; } + PhysicsDeformation physics = createPhysicsDeformation(side); + var model = side.isLeft ? lBoobArmor : rBoobArmor; var layer = RenderTypes.armorCutoutNoCull(texture); - queue.submitCustomGeometry(matrixStack, layer, new BreastRenderCommand(model, state, OverlayTexture.NO_OVERLAY, ARGB.opaque(color))); + queue.submitCustomGeometry(matrixStack, layer, new BreastRenderCommand(model, state, OverlayTexture.NO_OVERLAY, ARGB.opaque(color), physics)); if(glint) { - renderGlint(matrixStack, queue, state, model); + renderGlint(matrixStack, queue, state, model, side); } } @@ -210,16 +212,19 @@ protected void renderArmorTrim(ResourceKey armorModel, PoseStack var key = new EquipmentLayerRenderer.TrimSpriteKey(trim, EquipmentClientInfo.LayerType.HUMANOID, armorModel); TextureAtlasSprite sprite = ((EquipmentLayerRendererAccessor) equipmentRenderer).getTrimSpriteLookup().apply(key); + PhysicsDeformation physics = createPhysicsDeformation(side); + var layer = Sheets.armorTrimsSheet(trim.pattern().value().decal()); - queue.submitCustomGeometry(matrixStack, layer, BreastRenderCommand.trim(model, state, sprite)); + queue.submitCustomGeometry(matrixStack, layer, BreastRenderCommand.trim(model, state, sprite, physics)); if(glint) { - renderGlint(matrixStack, queue, state, model); + renderGlint(matrixStack, queue, state, model, side); } } - protected void renderGlint(PoseStack matrixStack, SubmitNodeCollector renderQueue, S state, BreastModelBox box) { + protected void renderGlint(PoseStack matrixStack, SubmitNodeCollector renderQueue, S state, BreastModelBox box, BreastSide side) { + PhysicsDeformation physics = createPhysicsDeformation(side); var glintLayer = RenderTypes.armorEntityGlint(); - renderQueue.submitCustomGeometry(matrixStack, glintLayer, new BreastRenderCommand(box, state, OverlayTexture.NO_OVERLAY, -1)); + renderQueue.submitCustomGeometry(matrixStack, glintLayer, new BreastRenderCommand(box, state, OverlayTexture.NO_OVERLAY, -1, physics)); } } diff --git a/src/main/java/com/wildfire/render/GenderLayer.java b/src/main/java/com/wildfire/render/GenderLayer.java index 162c8f89..c440c639 100644 --- a/src/main/java/com/wildfire/render/GenderLayer.java +++ b/src/main/java/com/wildfire/render/GenderLayer.java @@ -72,7 +72,7 @@ public class GenderLayer render) { super(render); @@ -155,15 +155,18 @@ protected boolean setupRender(S entityState, GenderRenderState genderState) { lPhysPositionY = leftPhysicsState.getPositionY(); lPhysPositionX = leftPhysicsState.getPositionX(); lPhysBounceRotation = leftPhysicsState.getBounceRotation(); + lPhysVelocityY = leftPhysicsState.getVelocityY(); if(isUniboob) { rPhysPositionY = lPhysPositionY; rPhysPositionX = lPhysPositionX; rPhysBounceRotation = lPhysBounceRotation; + rPhysVelocityY = lPhysVelocityY; } else { GenderRenderState.BreastPhysicsState rightPhysicsState = genderState.rightBreastPhysics; rPhysPositionY = rightPhysicsState.getPositionY(); rPhysPositionX = rightPhysicsState.getPositionX(); rPhysBounceRotation = rightPhysicsState.getBounceRotation(); + rPhysVelocityY = rightPhysicsState.getVelocityY(); } breastSize = Math.min(bSize * 1.5f, 0.7f); // Limit the max size to 0.7f @@ -220,27 +223,15 @@ protected void setupTransformations(S state, M model, PoseStack matrixStack, Bre matrixStack.mulPose(new Quaternionf().rotationZYX(body.zRot, body.yRot, body.xRot)); } - if(bounceEnabled) { - matrixStack.translate((side.isLeft ? lPhysPositionX : rPhysPositionX) / 32f, 0, 0); - matrixStack.translate(0, (side.isLeft ? lPhysPositionY : rPhysPositionY) / 32f, 0); - } + // physics translations now applied per-vertex in renderBox() matrixStack.translate((side.isLeft ? breastOffsetX : -breastOffsetX) * 0.0625f, 0.05625f + (breastOffsetY * 0.0625f), zOffset - 0.0625f * 2f + (breastOffsetZ * 0.0425f)); //shift down to correct position - if(!isUniboob) { - matrixStack.translate(-0.0625f * 2 * (side.isLeft ? 1 : -1), 0, 0); - } - if(bounceEnabled) { - matrixStack.mulPose(new Quaternionf().rotationXYZ(0, (float)((side.isLeft ? lPhysBounceRotation : rPhysBounceRotation) * (Math.PI / 180f)), 0)); - } - if(!isUniboob) { - matrixStack.translate(0.0625f * 2 * (side.isLeft ? 1 : -1), 0, 0); - } + // bounce Y-rotation now applied per-vertex in renderBox() float rotation = breastSize; if(bounceEnabled) { matrixStack.translate(0, -0.035f * breastSize, 0); //shift down to correct position - rotation -= (side.isLeft ? lPhysPositionY : rPhysPositionY) / 12f; } rotation = Math.min(rotation, breastSize + 0.2f); @@ -263,6 +254,17 @@ protected void setupTransformations(S state, M model, PoseStack matrixStack, Bre matrixStack.scale(0.9995f, 1f, 1f); //z-fighting FIXXX } + protected PhysicsDeformation createPhysicsDeformation(BreastSide side) { + if(!bounceEnabled) return PhysicsDeformation.NONE; + float bounceRotDeg = side.isLeft ? lPhysBounceRotation : rPhysBounceRotation; + return new PhysicsDeformation( + side.isLeft ? lPhysPositionX : rPhysPositionX, + side.isLeft ? lPhysPositionY : rPhysPositionY, + side.isLeft ? lPhysVelocityY : rPhysVelocityY, + bounceRotDeg * DEG_TO_RAD + ); + } + private void renderBreast(S state, PoseStack matrixStack, SubmitNodeCollector queue, int overlay, BreastSide side) { RenderType renderLayer = getRenderLayer(state); if(renderLayer == null) return; // only render if the player is visible in some capacity @@ -270,14 +272,16 @@ private void renderBreast(S state, PoseStack matrixStack, SubmitNodeCollector qu int alpha = state.isInvisible ? ARGB.as8BitChannel(0.15f) : 255; int color = ARGB.color(alpha, 255, 255, 255); + PhysicsDeformation physics = createPhysicsDeformation(side); + var model = side.isLeft ? lBreast : rBreast; - queue.submitCustomGeometry(matrixStack, renderLayer, new BreastRenderCommand(model, state, overlay, color)); + queue.submitCustomGeometry(matrixStack, renderLayer, new BreastRenderCommand(model, state, overlay, color, physics)); if(state instanceof AvatarRenderState playerState && playerState.showJacket) { matrixStack.translate(0, 0, -0.015f); matrixStack.scale(1.05f, 1.05f, 1.05f); var jacketModel = side.isLeft ? lBreastWear : rBreastWear; - queue.submitCustomGeometry(matrixStack, renderLayer, new BreastRenderCommand(jacketModel, state, overlay, color)); + queue.submitCustomGeometry(matrixStack, renderLayer, new BreastRenderCommand(jacketModel, state, overlay, color, physics)); } } @@ -300,14 +304,37 @@ protected void renderSides(S state, M model, PoseStack matrixStack, Consumer 0) { + var verts = quad.vertexPositions; + if(verts[0].y() == yMin && verts[1].y() == yMin && verts[2].y() == yMin && verts[3].y() == yMin) { + continue; + } + } + Vector3f vector3f = new Vector3f(quad.normal.x(), quad.normal.y(), quad.normal.z()).mul(matrix3f); float normalX = vector3f.x; float normalY = vector3f.y; @@ -316,6 +343,28 @@ public static void renderBox(WildfireModelRenderer.ModelBox model, PoseStack.Pos float j = vertex.x() / 16.0F; float k = vertex.y() / 16.0F; float l = vertex.z() / 16.0F; + + //per-vertex weighted deformation: 0 at attachment (posY1), 1 at tip (posY2) + if(yRange > 0) { + float weight = Mth.clamp((vertex.y() - yMin) / yRange, 0f, 1f); + + j += (physTransX + physSwayX) * weight; + k += physTransY * weight; + + //weighted X-rotation tilt, pivots around attachment edge + { + float localY = k - (yMin / 16.0F); + float localZ = l; + float rotatedY = localY * cosPhysRotX - localZ * sinPhysRotX; + float rotatedZ = localZ * cosPhysRotX + localY * sinPhysRotX; + k = (yMin / 16.0F) + localY + (rotatedY - localY) * weight; + l = localZ + (rotatedZ - localZ) * weight; + } + + float attachY = yMin / 16.0F; //squash-and-stretch from attachment + k = attachY + (k - attachY) * stretchFactor; + } + Vector4f vector4f = new Vector4f(j, k, l, 1.0F).mul(matrix4f); vertexConsumer.addVertex(vector4f.x(), vector4f.y(), vector4f.z(), color, vertex.u(), vertex.v(), overlay, light, normalX, normalY, normalZ); diff --git a/src/main/java/com/wildfire/render/GenderRenderState.java b/src/main/java/com/wildfire/render/GenderRenderState.java index e81fef03..1ba6644e 100644 --- a/src/main/java/com/wildfire/render/GenderRenderState.java +++ b/src/main/java/com/wildfire/render/GenderRenderState.java @@ -143,6 +143,7 @@ public class BreastPhysicsState { private final float prePositionX, positionX; private final float preBounceRotation, bounceRotation; private final float preBreastSize, breastSize; + private final float preVelocity, velocityY; private BreastPhysicsState(BreastPhysics breastPhysics) { this.prePositionY = breastPhysics.getPrePositionY(); @@ -153,6 +154,8 @@ private BreastPhysicsState(BreastPhysics breastPhysics) { this.bounceRotation = breastPhysics.getBounceRotation(); this.preBreastSize = breastPhysics.getPreBreastSize(); this.breastSize = breastPhysics.getBreastSize(); + this.preVelocity = breastPhysics.getPreVelocity(); + this.velocityY = breastPhysics.getVelocity(); } public float getPositionY() { @@ -170,5 +173,9 @@ public float getBounceRotation() { public float getBreastSize() { return Mth.lerp(partialTicks, this.preBreastSize, this.breastSize); } + + public float getVelocityY() { + return Mth.lerp(partialTicks, this.preVelocity, this.velocityY); + } } } diff --git a/src/main/java/com/wildfire/render/PhysicsDeformation.java b/src/main/java/com/wildfire/render/PhysicsDeformation.java new file mode 100644 index 00000000..70c782a2 --- /dev/null +++ b/src/main/java/com/wildfire/render/PhysicsDeformation.java @@ -0,0 +1,44 @@ +/* + * Wildfire's Female Gender Mod is a female gender mod created for Minecraft. + * Copyright (C) 2023-present WildfireRomeo + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.wildfire.render; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; + +/** + * Per-vertex physics deformation parameters for breast rendering. + * + *

Instead of applying physics offsets as uniform translations (which moves the entire breast + * including the base that should stay attached to the body), these values are applied per-vertex + * weighted by the vertex's Y-distance from the attachment edge. This creates a natural bending/stretching + * effect where the base stays anchored and only the tip moves with the physics.

+ * + * @param offsetX Horizontal sway offset (from {@code BreastPhysics.positionX}) + * @param offsetY Vertical bounce offset (from {@code BreastPhysics.positionY}) + * @param velocityY Spring velocity for squash-and-stretch effect (from {@code BreastPhysics.velocity}) + * @param bounceRotY Y-axis bounce rotation in radians (lateral sway from {@code BreastPhysics.bounceRotation}). + * Applied per-vertex weighted so the attachment edge stays anchored. + */ +@Environment(EnvType.CLIENT) +public record PhysicsDeformation(float offsetX, float offsetY, float velocityY, float bounceRotY) { + /** + * A no-op deformation that applies no physics effects. Used when bounce physics are disabled. + */ + public static final PhysicsDeformation NONE = new PhysicsDeformation(0, 0, 0, 0); +}