Compare commits

...
Author SHA1 Message Date
Landon Wills 09a0cd6c30 Start restructure of camera transformations 2025-04-27 07:46:20 -04:00
6 changed files with 378 additions and 52 deletions
@@ -20,6 +20,7 @@ import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix3f;
import org.joml.Quaternionf;
@@ -34,31 +35,49 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Shadow private Entity entity;
@Shadow @Final private Quaternionf rotation;
@Shadow @Final private Vector3f forwards;
@Shadow @Final private Vector3f up;
@Shadow @Final private Vector3f left;
@Shadow private float xRot;
@Shadow private float yRot;
@Shadow
private Entity entity;
@Shadow
@Final
private Quaternionf rotation;
@Shadow
@Final
private Vector3f forwards;
@Shadow
@Final
private Vector3f up;
@Shadow
@Final
private Vector3f left;
@Shadow
private float xRot;
@Shadow
private float yRot;
@Shadow private float eyeHeight;
@Shadow private float eyeHeightOld;
@Shadow
private float eyeHeight;
@Shadow
private float eyeHeightOld;
@Shadow protected abstract void setPosition(Vec3 pos);
@Shadow
protected abstract void setPosition(Vec3 pos);
@Unique private float returnTimer = 0;
@Unique private float rollAngleStart = 0;
@Unique Minecraft minecraft = Minecraft.getInstance();
@Unique
private float returnTimer = 0;
@Unique
private float rollAngleStart = 0;
@Unique
Minecraft minecraft = Minecraft.getInstance();
@Unique boolean disableRollableCameraDebug = false;
@Unique
boolean disableRollableCameraDebug = false;
@Inject(method = "setRotation", at = @At("HEAD"), cancellable = true)
public void cobblemon$setRotation(float f, float g, CallbackInfo ci) {
if (!(this.entity instanceof OrientationControllable controllable) || disableRollableCameraDebug) return;
var controller = controllable.getOrientationController();
if (!controller.isActive() && controller.getOrientation() != null) {
if(this.returnTimer < 1) {
if (this.returnTimer < 1) {
//Rotation is taken from entity since we no longer handle mouse ourselves
//Stops a period of time when you can't input anything.
controller.getOrientation().set(
@@ -66,16 +85,17 @@ public abstract class CameraMixin {
.rotateY((float) Math.toRadians(180 - this.entity.getYRot()))
.rotateX((float) Math.toRadians(-this.entity.getXRot()))
);
if(rollAngleStart == 0){
if (rollAngleStart == 0) {
this.returnTimer = 1;
controller.reset();
return;
}
controller.getOrientation().rotateZ((float) Math.toRadians(-rollAngleStart*(1-returnTimer)));
controller.getOrientation().rotateZ((float) Math.toRadians(-rollAngleStart * (1 - returnTimer)));
cobblemon$applyRotation();
this.returnTimer += .05F;
ci.cancel();
} else {
}
else {
this.returnTimer = 1;
controller.reset();
}
@@ -89,26 +109,16 @@ public abstract class CameraMixin {
ci.cancel();
}
@Inject(method = "setup", at = @At("HEAD"))
private void cobblemon$tickAnimationPreRenders(BlockGetter level, Entity entity, boolean detached, boolean thirdPersonReverse, float partialTick, CallbackInfo ci) {
MountedPokemonAnimationRenderController.INSTANCE.reset();
}
@WrapOperation(method = "setup", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setPosition(DDD)V"))
public void cobblemon$positionCamera(Camera instance, double x, double y, double z, Operation<Void> original, @Local(ordinal = 1, argsOnly = true) boolean thirdPersonReverse) {
Entity entity = instance.getEntity();
Entity vehicle = entity.getVehicle();
MountedPokemonAnimationRenderController.INSTANCE.reset();
if(vehicle instanceof Rideable){
Vec3 position = MountedCameraRenderer.INSTANCE.getCameraPosition(
instance,
entity,
vehicle,
x,
y,
z,
thirdPersonReverse,
eyeHeightOld,
eyeHeight
);
if(position != null){
if (cobblemon$isRidingPokemon(entity)) {
Vec3 position = MountedCameraRenderer.INSTANCE.getCameraPosition(instance, entity, entity.getVehicle(), thirdPersonReverse, eyeHeightOld, eyeHeight);
if (position != null) {
setPosition(position);
return;
}
@@ -118,12 +128,18 @@ public abstract class CameraMixin {
}
@Unique
private void cobblemon$applyRotation(){
if (!(this.entity instanceof OrientationControllable controllable) || controllable.getOrientationController().getOrientation() == null) return;
private boolean cobblemon$isRidingPokemon(Entity entity) {
return entity.getVehicle() instanceof PokemonEntity;
}
@Unique
private void cobblemon$applyRotation() {
if (!(this.entity instanceof OrientationControllable controllable) || controllable.getOrientationController().getOrientation() == null)
return;
var controller = controllable.getOrientationController();
var newRotation = controller.getOrientation().normal(new Matrix3f()).getNormalizedRotation(new Quaternionf());
if (this.minecraft.options.getCameraType().isMirrored()) {
newRotation.rotateY((float)Math.toRadians(180));
newRotation.rotateY((float) Math.toRadians(180));
}
this.rotation.set(newRotation);
this.xRot = controller.getPitch();
@@ -138,7 +154,7 @@ public abstract class CameraMixin {
Entity entity = instance.getEntity();
Entity vehicle = entity.getVehicle();
if (!(vehicle instanceof PokemonEntity) || Cobblemon.config.getThirdPersonViewBobbing()) {
if (!(vehicle instanceof PokemonEntity) || Cobblemon.config.getViewBobTranslation()) {
original.call(instance, zoom, dy, dx);
}
}
@@ -0,0 +1,308 @@
package com.cobblemon.mod.common.client.render.camera
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.OrientationControllable
import com.cobblemon.mod.common.api.riding.Seat
import com.cobblemon.mod.common.client.MountedPokemonAnimationRenderController
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository.getPoser
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.mixin.accessor.CameraAccessor
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.toVec3d
import net.minecraft.client.Camera
import net.minecraft.client.Minecraft
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth
import net.minecraft.world.entity.Entity
import net.minecraft.world.level.ClipContext
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import org.joml.Matrix3f
import org.joml.Quaternionf
import org.joml.Vector3f
object CameraTransformationController {
private val transformers = mutableMapOf<Int, MutableList<CameraTransformer>>(
1 to mutableListOf(RidingCameraTransformer()),
)
fun register(priority: Int, transformer: CameraTransformer) {
if (transformers[priority] == null) {
transformers[priority] = mutableListOf()
}
transformers[priority]!!.add(transformer)
}
fun tick(current: CameraTransformation, delta: Float, context: CameraContext): CameraTransformation {
val transformers = transformers.toSortedMap().reversed().values.flatten()
transformers.forEach {
if (it.condition(context)) {
return it.tick(current, delta, context)
}
}
return current
}
}
interface CameraTransformer {
val key: ResourceLocation
val condition: (CameraContext) -> Boolean
fun tick(current: CameraTransformation, delta: Float, context: CameraContext): CameraTransformation
fun onStart() {}
fun onStop() {}
}
data class CameraTransformation(val position: Vec3, val rotation: Quaternionf, val fov: Int)
data class CameraContext(
val camera: Camera,
val entity: Entity,
val eyeHeight: Float,
val eyeHeightO: Float,
val perspective: CameraPerspective
)
enum class CameraPerspective {
FIRST_PERSON,
THIRD_PERSON_BACK,
THIRD_PERSON_FRONT
}
class RidingCameraTransformer : CameraTransformer {
override val key = cobblemonResource("camera/riding")
override val condition: (CameraContext) -> Boolean = { it.entity.vehicle is PokemonEntity }
val positionTransformer = RidingCameraPositionTransformer()
val viewBobPositionTransformer = RidingCameraViewBobPositionTransformer()
val rotationTransformer = OrientationControllerRotationTransformer()
val viewBobRotationTransformer = RidingCameraViewBobRotationTransformer()
override fun tick(current: CameraTransformation, delta: Float, context: CameraContext): CameraTransformation {
val vehicle = context.entity.vehicle as? PokemonEntity ?: return current
MountedPokemonAnimationRenderController.setup(vehicle, delta)
val position = getPosition(delta, context, vehicle)
val rotation = getRotation(current, context)
val fov = Minecraft.getInstance().options.fov().get()
return CameraTransformation(position, rotation, fov)
}
private fun getPosition(delta: Float, context: CameraContext, vehicle: PokemonEntity): Vec3 {
if (context.perspective == CameraPerspective.FIRST_PERSON || Cobblemon.config.viewBobTranslation) {
return viewBobPositionTransformer.getPosition(delta, context, vehicle)
} else {
return positionTransformer.getPosition(delta, context, vehicle)
}
}
private fun getRotation(current: CameraTransformation, context: CameraContext): Quaternionf {
if (Cobblemon.config.viewBobRotation) {
return viewBobRotationTransformer.getRotation()
}
else {
return rotationTransformer.getRotation(current.rotation, context)
}
}
}
class RidingCameraPositionTransformer {
fun getPosition(delta: Float, context: CameraContext, vehicle: PokemonEntity): Vec3 {
val position = Vec3(
Mth.lerp(delta.toDouble(), vehicle.xOld, vehicle.x),
Mth.lerp(delta.toDouble(), vehicle.yOld, vehicle.y),
Mth.lerp(delta.toDouble(), vehicle.zOld, vehicle.z)
)
val orientation = getOrientation(context.entity)
var offset = Vec3.ZERO
offset = applyThirdPersonToOffset(offset, context, vehicle)
offset = applyRollingToOffset(offset, orientation)
offset = applyZoomToOffset(offset, position, context)
val pivotOffset = applyPivotToOffset(vehicle, context.entity, orientation)
return position.add(pivotOffset).add(offset)
}
fun applyPivotToOffset(vehicle: PokemonEntity, passenger: Entity, orientation: Matrix3f): Vec3 {
val seatIndex: Int = vehicle.passengers.indexOf(passenger)
if (seatIndex < 0) return Vec3.ZERO
val seat: Seat = vehicle.seats[seatIndex]
val model = getPoser(vehicle.pokemon.species.resourceIdentifier, FloatingState())
val center = Vec3(0.0, passenger.bbHeight / 2.0, 0.0)
val pivotOffsets = model.thirdPersonPivotOffset
if (!pivotOffsets.containsKey(seat.locator)) return Vec3.ZERO
val pivotOffset = pivotOffsets[seat.locator]!!
val rotatedOffset = orientation.transform(pivotOffset.subtract(center).toVector3f())
return rotatedOffset.toVec3d().add(center)
}
fun applyThirdPersonToOffset(offset: Vec3, context: CameraContext, vehicle: PokemonEntity): Vec3 {
if (context.perspective == CameraPerspective.FIRST_PERSON) return offset
val model = getPoser(vehicle.pokemon.species.resourceIdentifier, FloatingState())
val cameraOffsets = model.thirdPersonCameraOffset
val seatIndex: Int = vehicle.passengers.indexOf(context.entity)
if (seatIndex < 0) return offset
val seat: Seat = vehicle.seats[seatIndex]
val thirdPartyOffset = getThirdPartyOffset(seat, cameraOffsets, context)
return offset.add(thirdPartyOffset)
}
private fun getThirdPartyOffset(seat: Seat, offsets: MutableMap<String, Vec3>, context: CameraContext): Vec3 {
val locatorName = when (context.perspective) {
CameraPerspective.FIRST_PERSON -> seat.locator
CameraPerspective.THIRD_PERSON_FRONT -> seat.locator
CameraPerspective.THIRD_PERSON_BACK -> "${seat.locator}_reverse"
}
if (!offsets.containsKey(locatorName)) return Vec3.ZERO
val offset = offsets[locatorName]!!
return when (context.perspective) {
CameraPerspective.FIRST_PERSON -> Vec3.ZERO
CameraPerspective.THIRD_PERSON_FRONT -> offset
CameraPerspective.THIRD_PERSON_BACK -> offset.multiply(1.0, 1.0, -1.0)
}
}
fun applyRollingToOffset(offset: Vec3, orientation: Matrix3f): Vec3 {
return orientation.transform(offset.toVector3f()).toVec3d()
}
fun applyZoomToOffset(offset: Vec3, pivot: Vec3, context: CameraContext): Vec3 {
val offsetDistance = offset.length()
val offsetDirection = offset.multiply(1 / offsetDistance, 1 / offsetDistance, 1 / offsetDistance)
val maxZoom = getMaxZoom(offsetDistance.toFloat(), offsetDirection.toVector3f(), pivot.toVector3f(), context.camera).toDouble()
return offset.multiply(maxZoom, maxZoom, maxZoom)
}
private fun getMaxZoom(maxZoom: Float, directionVector: Vector3f, positionVector: Vector3f, instance: Camera): Float {
var maxZoom = maxZoom
val pos = Vec3(positionVector)
for (i in 0 .. 7) {
val g = ((i and 1) * 2 - 1).toFloat()
val h = ((i shr 1 and 1) * 2 - 1).toFloat()
val j = ((i shr 2 and 1) * 2 - 1).toFloat()
val vec3 = pos.add((g * 0.1f).toDouble(), (h * 0.1f).toDouble(), (j * 0.1f).toDouble())
val vec32 = vec3.add(Vec3(directionVector).scale(maxZoom.toDouble()))
val level = (instance as CameraAccessor).level
val hitResult: HitResult =
level.clip(ClipContext(vec3, vec32, ClipContext.Block.VISUAL, ClipContext.Fluid.NONE, instance.entity))
if (hitResult.type != HitResult.Type.MISS) {
val k = hitResult.getLocation().distanceToSqr(pos).toFloat()
if (k < Mth.square(maxZoom)) {
maxZoom = Mth.sqrt(k)
}
}
}
return maxZoom
}
fun getOrientation(passenger: Entity): Matrix3f {
val xRot = (-1 * Math.toRadians(passenger.xRot.toDouble())).toFloat()
val controller = (passenger as? OrientationControllable)?.getOrientationController()
if (controller == null || !controller.isActive() || controller.orientation == null) {
return Matrix3f()
.rotateY(Math.toRadians((180f - passenger.yRot).toDouble()).toFloat())
.rotateX(xRot)
}
return controller.orientation!!
}
}
class RidingCameraViewBobPositionTransformer {
fun getPosition(delta: Float, context: CameraContext, vehicle: PokemonEntity): Vec3 {
val position = Vec3(
Mth.lerp(delta.toDouble(), vehicle.xOld, vehicle.x),
Mth.lerp(delta.toDouble(), vehicle.yOld, vehicle.y),
Mth.lerp(delta.toDouble(), vehicle.zOld, vehicle.z)
)
var offset = Vec3.ZERO
// Add eye level
offset = applyAABBToOffset(context, delta, offset, context.entity)
// Add first person offset from poser if found
offset = applyFirstPersonToOffset(context, offset, vehicle, context.entity)
// Transform the offset based on the entity's orientation
offset = applyRollingToOffset(offset, delta, context.entity)
// Add the offset from the locator
offset = applyLocatorToOffset(offset, vehicle, context.entity)
return position.add(offset)
}
private fun applyLocatorToOffset(offset: Vec3, vehicle: PokemonEntity, passenger: Entity): Vec3 {
val seatIndex: Int = vehicle.passengers.indexOf(passenger)
if (seatIndex < 0) return offset
val seat: Seat = vehicle.seats[seatIndex]
val delegate = vehicle.delegate as PokemonClientDelegate
val locator = delegate.locatorStates[seat.locator] ?: return offset
return offset.add(Vec3(locator.matrix.getTranslation(Vector3f())))
}
private fun applyAABBToOffset(context: CameraContext, delta: Float, offset: Vec3, passenger: Entity): Vec3 {
val currEyeHeight: Double =
Mth.lerp(delta.toDouble(), context.eyeHeight.toDouble(), context.eyeHeightO.toDouble())
return offset.add(0.0, currEyeHeight - (passenger.bbHeight / 2), 0.0)
}
private fun applyRollingToOffset(offset: Vec3, delta: Float, passenger: Entity): Vec3 {
val rollable = passenger as? OrientationControllable ?: return offset
val controller = rollable.getOrientationController()
if (!controller.isActive()) return offset
return controller.getRenderOrientation(delta).transform(offset.toVector3f()).toVec3d()
}
private fun applyFirstPersonToOffset(
context: CameraContext,
offset: Vec3,
vehicle: PokemonEntity,
passenger: Entity
): Vec3 {
if (context.perspective != CameraPerspective.FIRST_PERSON) return offset
val model = getPoser(vehicle.pokemon.species.resourceIdentifier, FloatingState())
val seatIndex: Int = vehicle.passengers.indexOf(passenger)
val seat: Seat = vehicle.seats[seatIndex]
val cameraOffsets = model.firstPersonCameraOffset
return when (cameraOffsets.containsKey(seat.locator)) {
true -> cameraOffsets[seat.locator]!!
false -> Vec3.ZERO
}
}
}
class RidingCameraViewBobRotationTransformer {
fun getRotation(): Quaternionf {
TODO()
}
}
class OrientationControllerRotationTransformer {
fun getRotation(current: Quaternionf, context: CameraContext): Quaternionf {
val entity = context.entity
val controllable = entity as? OrientationControllable ?: return current
val controller = controllable.getOrientationController()
val orientation = controller.orientation ?: return current
return orientation.normal(Matrix3f()).getNormalizedRotation(Quaternionf())
}
}
@@ -33,9 +33,6 @@ object MountedCameraRenderer
instance: Camera,
entity: Entity,
vehicle: Entity,
x: Double,
y: Double,
z: Double,
thirdPersonReverse: Boolean,
eyeHeightOld: Double,
eyeHeight: Double
@@ -59,7 +56,7 @@ object MountedCameraRenderer
// First person and view bobbing are handled the same way.
if (!instance.isDetached || Cobblemon.config.thirdPersonViewBobbing) {
if (!instance.isDetached || Cobblemon.config.viewBobTranslation) {
val delegate = vehicle.delegate as PokemonClientDelegate
val locator = delegate.locatorStates[seat.locator]
@@ -176,4 +173,4 @@ object MountedCameraRenderer
return maxZoom
}
}
}
@@ -262,8 +262,11 @@ class CobblemonConfig {
@CobblemonConfigField(Category.Interface, lang = "party_portrait_animations")
var partyPortraitAnimations = PortraitStyle.NEVER_ANIMATE
@CobblemonConfigField(Category.Riding, lang = "third_person_view_bobbing")
var thirdPersonViewBobbing = false
@CobblemonConfigField(Category.Riding, lang = "view_bob_translation")
var viewBobTranslation = false
@CobblemonConfigField(Category.Riding, lang = "view_bob_rotation")
var viewBobRotation = false
@CobblemonConfigField(Category.Riding, lang = "invert_roll")
var invertRoll = false
@@ -15,7 +15,7 @@ import com.cobblemon.mod.common.util.readEnumConstant
import com.cobblemon.mod.common.util.writeEnumConstant
import net.minecraft.network.RegistryFriendlyByteBuf
class RideBoostsUpdatePacket(pokemon: () -> Pokemon, rideBoosts: Map<RidingStat, Float>) : SingleUpdatePacket<Map<RidingStat, Float>, RideBoostsUpdatePacket>(pokemon, rideBoosts) {
class RideBoostsUpdatePacket(pokemon: () -> Pokemon?, rideBoosts: Map<RidingStat, Float>) : SingleUpdatePacket<Map<RidingStat, Float>, RideBoostsUpdatePacket>(pokemon, rideBoosts) {
companion object {
val ID = cobblemonResource("ride_boosts_update")
fun decode(buffer: RegistryFriendlyByteBuf): RideBoostsUpdatePacket {
@@ -37,4 +37,4 @@ class RideBoostsUpdatePacket(pokemon: () -> Pokemon, rideBoosts: Map<RidingStat,
override fun set(pokemon: Pokemon, value: Map<RidingStat, Float>) {
pokemon.setRideBoosts(value)
}
}
}
@@ -2431,8 +2431,10 @@
"cobblemon.config.ui.party_portrait_animations.animate_selected.tooltip": "Selected Pokémon animates unless fainted",
"cobblemon.config.ui.party_portrait_animations.always_animate": "Always animate",
"cobblemon.config.ui.party_portrait_animations.always_animate.tooltip": "Entire party animates unless fainted",
"cobblemon.config.ui.third_person_view_bobbing": "Third Person View Bobbing",
"cobblemon.config.ui.third_person_view_bobbing.tooltip": "If true then use default Minecraft behavior to follow directly behind the player's head. If false then follow a fixed offset from the pokemon.",
"cobblemon.config.ui.view_bob_translation": "View Bob Translation",
"cobblemon.config.ui.view_bob_translation.tooltip": "If true then use default Minecraft behavior to follow directly behind the player's head. If false then follow a fixed offset from the pokemon.",
"cobblemon.config.ui.view_bob_rotation": "View Bob Rotation",
"cobblemon.config.ui.view_bob_rotation.tooltip": "If true then will rotate the camera based on the Pokemon's movements. If false then no rotation will occur.",
"cobblemon.config.ui.invert_pitch": "Invert Pitch",
"cobblemon.config.ui.invert_pitch.tooltip": "Invert the direction for pitch (left and right).",
"cobblemon.config.ui.invert_yaw": "Invert Yaw",
@@ -8899,4 +8901,4 @@
"cobblemon.ability.teraformzero.desc": "When Terapagos changes into its Stellar Form, it uses its hidden powers to eliminate all effects of weather and terrain, reducing them to zero.",
"cobblemon.ability.poisonpuppeteer": "Poison Puppeteer",
"cobblemon.ability.poisonpuppeteer.desc": "Pokémon poisoned by Pecharunt's moves will also become confused."
}
}