Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/main/java/com/wildfire/physics/BreastPhysics.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down
15 changes: 10 additions & 5 deletions src/main/java/com/wildfire/render/BreastRenderCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,26 @@ public record BreastRenderCommand(
int overlay,
int color,
int outline,
@Nullable UnaryOperator<VertexConsumer> consumerOperator
@Nullable UnaryOperator<VertexConsumer> 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
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);
}
}
17 changes: 11 additions & 6 deletions src/main/java/com/wildfire/render/GenderArmorLayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -210,16 +212,19 @@ protected void renderArmorTrim(ResourceKey<EquipmentAsset> 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason why you're creating this again instead of providing the instance the caller already has?

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));
}
}
85 changes: 67 additions & 18 deletions src/main/java/com/wildfire/render/GenderLayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public class GenderLayer<S extends HumanoidRenderState, M extends HumanoidModel<
protected IGenderArmor genderArmor = IGenderArmor.EMPTY;
protected boolean isChestplateOccupied, bounceEnabled, breathingAnimation;
protected float breastOffsetX, breastOffsetY, breastOffsetZ, lPhysPositionY, lPhysPositionX, rPhysPositionY, rPhysPositionX,
lPhysBounceRotation, rPhysBounceRotation, breastSize, zOffset, outwardAngle;
lPhysBounceRotation, rPhysBounceRotation, lPhysVelocityY, rPhysVelocityY, breastSize, zOffset, outwardAngle;

public GenderLayer(RenderLayerParent<S, M> render) {
super(render);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -263,21 +254,34 @@ 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

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));
}
}

Expand All @@ -300,14 +304,37 @@ protected void renderSides(S state, M model, PoseStack matrixStack, Consumer<Bre
}

public static void renderBox(WildfireModelRenderer.ModelBox model, PoseStack.Pose entry, VertexConsumer vertexConsumer,
int light, int overlay, int color) {
int light, int overlay, int color, PhysicsDeformation physics) {
Matrix4f matrix4f = entry.pose();
Matrix3f matrix3f = entry.normal();

// weight deformation by Y-distance: Y=posY1 (top) is attachment, Y=posY2 (bottom) is tip
float yMin = model.posY1;
float yRange = model.posY2 - model.posY1;
float physTransX = physics.offsetX() / 16f;
float physTransY = physics.offsetY() / 16f;
float physRotXRad = (physics.offsetY() / 8f) * 35f * DEG_TO_RAD;
float sinPhysRotX = (float) Math.sin(physRotXRad);
float cosPhysRotX = (float) Math.cos(physRotXRad);
// lateral sway as translation instead of rotation to prevent corners poking out
float avgZ = (model.posZ1 + model.posZ2) / 2f / 16f;
float physSwayX = (float) Math.sin(physics.bounceRotY()) * avgZ;
float stretchFactor = 1.0f + physics.velocityY() * 0.06f; //squash-and-stretch
stretchFactor = Mth.clamp(stretchFactor, 0.80f, 1.20f);

for(var quad : model.quads) {

//Make sure UVs aren't set to zero. If they are, the textures screw up. Don't render the quad at all.
if(quad.uvs[0] == 0.0F && quad.uvs[1] == 0.0F && quad.uvs[2] == 0.0F && quad.uvs[3] == 0.0F) continue;

//Skip the attachment face to avoid shadow acne
if(yRange > 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;
Expand All @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/wildfire/render/GenderRenderState.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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() {
Expand All @@ -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);
}
}
}
44 changes: 44 additions & 0 deletions src/main/java/com/wildfire/render/PhysicsDeformation.java
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

package com.wildfire.render;

import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;

/**
* Per-vertex physics deformation parameters for breast rendering.
*
* <p>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.</p>
*
* @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);
}
Loading