diff --git a/ChestPreview/ChestPreview/Rendering/WorldBillboardPresenter.cs b/ChestPreview/ChestPreview/Rendering/WorldBillboardPresenter.cs index ca773ec..4764f5d 100644 --- a/ChestPreview/ChestPreview/Rendering/WorldBillboardPresenter.cs +++ b/ChestPreview/ChestPreview/Rendering/WorldBillboardPresenter.cs @@ -1,329 +1,329 @@ -using ChestPreview.Configs; -using ChestPreview.Models; -using ChestPreview.Utils; -using System; -using System.Collections.Generic; -using System.Numerics; -using Vintagestory.API.Client; -using Vintagestory.API.Common; -using Vintagestory.API.MathTools; -using Vintagestory.GameContent; - -namespace ChestPreview.Rendering; - -internal class WorldBillboardPresenter(ICoreClientAPI api) -{ - private const int multiblock_scan_radius = 2; - private static readonly float front_face_offset = 0.01f; - - private readonly ICoreClientAPI api = api; - private readonly List frameBillboards = []; - - public IReadOnlyList FrameBillboards => frameBillboards; - - public void PrepareFrame(List previewTargets) - { - frameBillboards.Clear(); - - foreach (PreviewTarget previewTarget in previewTargets) - { - if (!CanPresentAsBillboard(previewTarget)) - continue; - - if (!TryCreateBillboardTarget(previewTarget, out BillboardTarget billboardTarget)) - continue; - - frameBillboards.Add(billboardTarget); - } - } - - public void Clear() - { - frameBillboards.Clear(); - } - - private static bool CanPresentAsBillboard(PreviewTarget previewTarget) - { - return previewTarget.Mode is PreviewModes.ON_HOVERED_CONTAINER or PreviewModes.ON_NEARBY_CONTAINERS; - } - - private bool TryCreateBillboardTarget(PreviewTarget previewTarget, out BillboardTarget billboardTarget) - { - return TryCreateFrontBillboardTarget(previewTarget, out billboardTarget); - } - - private bool TryCreateFrontBillboardTarget(PreviewTarget previewTarget, out BillboardTarget billboardTarget) - { - billboardTarget = default; - - Block block = previewTarget.Block; - BlockPos blockPos = previewTarget.BlockEntity.Pos; - GetContainerBounds(block, blockPos, out Vector3 min, out Vector3 max); - - if (!TryResolveFacingNormal(previewTarget.Block, previewTarget.BlockEntity, out Vec3f forward, out Vec3f right, out Vec3f up)) - return false; - - Vector3 centerLocal = (min + max) * 0.5f; - Vector3 halfExtents = (max - min) * 0.5f; - Vector3 rightVector = right.ToVector3(); - Vector3 upVector = up.ToVector3(); - Vector3 forwardVector = forward.ToVector3(); - - float halfWidth = Vector3.Dot(Vector3.Abs(rightVector), halfExtents); - float halfHeight = Vector3.Dot(Vector3.Abs(upVector), halfExtents); - float halfDepth = ResolveStableHalfDepth(forwardVector, halfExtents); - - float width = halfWidth * 2f; - float height = halfHeight * 2f; - - Vector3 center = blockPos.ToVector3() + centerLocal + forwardVector * (halfDepth + front_face_offset); - - width = Math.Max(0.05f, width); - height = Math.Max(0.05f, height); - - billboardTarget = new BillboardTarget(previewTarget, center.ToVec3d(), right, up, forward, width, height, true); - return true; - } - - private static float ResolveStableHalfDepth(Vector3 forward, Vector3 halfExtents) - { - if (MathF.Abs(forward.Y) < 0.5f) - return MathF.Abs(forward.X) >= MathF.Abs(forward.Z) ? halfExtents.X : halfExtents.Z; - - return MathF.Abs(forward.Y) >= MathF.Abs(forward.Z) ? halfExtents.Y : halfExtents.Z; - } - - private void GetContainerBounds(Block block, BlockPos blockPos, out Vector3 min, out Vector3 max) - { - min = new Vector3(0f, 0f, 0f); - max = new Vector3(1f, 1f, 1f); - - ExpandBoundsFromSelectionBoxes(block, blockPos, blockPos, ref min, ref max); - ExpandBoundsFromLinkedMultiblockParts(blockPos, ref min, ref max); - } - - private void ExpandBoundsFromLinkedMultiblockParts(BlockPos controllerPos, ref Vector3 min, ref Vector3 max) - { - int minScanX = controllerPos.X - multiblock_scan_radius; - int maxScanX = controllerPos.X + multiblock_scan_radius; - int minScanY = controllerPos.Y - multiblock_scan_radius; - int maxScanY = controllerPos.Y + multiblock_scan_radius; - int minScanZ = controllerPos.Z - multiblock_scan_radius; - int maxScanZ = controllerPos.Z + multiblock_scan_radius; - Vector3 minLocal = min; - Vector3 maxLocal = max; - - api.World.BlockAccessor.WalkBlocks(new BlockPos(minScanX, minScanY, minScanZ), new BlockPos(maxScanX, maxScanY, maxScanZ), (partBlock, x, y, z) => - { - if (x == controllerPos.X && y == controllerPos.Y && z == controllerPos.Z) - return; - - if (partBlock.Id == 0 || partBlock is not IMultiblockOffset multiblockOffset) - return; - - BlockPos partPos = new(x, y, z); - BlockPos linkedControllerPos = multiblockOffset.GetControlBlockPos(partPos); - if (!IsSameBlockPos(linkedControllerPos, controllerPos)) - return; - - ExpandBoundsFromSelectionBoxes(partBlock, partPos, controllerPos, ref minLocal, ref maxLocal); - }); - - min = minLocal; - max = maxLocal; - } - - private void ExpandBoundsFromSelectionBoxes(Block block, BlockPos sourcePos, BlockPos originPos, ref Vector3 min, ref Vector3 max) - { - Cuboidf[]? selectionBoxes = block.GetSelectionBoxes(api.World.BlockAccessor, sourcePos); - Vector3 sourceOffset = sourcePos.ToVector3() - originPos.ToVector3(); - if (selectionBoxes is not { Length: > 0 }) - { - min = Vector3.Min(min, sourceOffset); - max = Vector3.Max(max, sourceOffset + Vector3.One); - return; - } - - foreach (Cuboidf selectionBox in selectionBoxes) - { - min = Vector3.Min(min, sourceOffset + selectionBox.Start.ToVector3()); - max = Vector3.Max(max, sourceOffset + selectionBox.End.ToVector3()); - } - } - - private static bool IsSameBlockPos(BlockPos a, BlockPos b) - { - return a.X == b.X && a.Y == b.Y && a.Z == b.Z; - } - - private bool TryResolveFacingNormal(Block block, BlockEntity blockEntity, out Vec3f forward, out Vec3f right, out Vec3f up) - { - forward = new Vec3f(0f, 0f, 1f); - right = new Vec3f(1f, 0f, 0f); - up = new Vec3f(0f, 1f, 0f); - - if (TryGetMeshAngle(blockEntity, out float meshAngleRadians)) - { - // MeshAngle is around Y axis in radians. - forward = new Vec3f(MathF.Sin(meshAngleRadians), 0f, MathF.Cos(meshAngleRadians)); - NormalizeFacing(ref forward); - BuildBasisFromForward(forward, out right, out up); - return true; - } - - if (TryGetBlockSideFacing(block, out forward)) - { - BuildBasisFromForward(forward, out right, out up); - return true; - } - - if (!TryGetPlayerFacing(blockEntity.Pos, out forward)) - return false; - - BuildBasisFromForward(forward, out right, out up); - return true; - } - - private bool TryGetPlayerFacing(BlockPos blockPos, out Vec3f facing) - { - facing = new Vec3f(0f, 0f, 1f); - - EntityPlayer? playerEntity = api.World.Player?.Entity; - if (playerEntity?.CameraPos == null) - return false; - - Vec3d cameraPos = playerEntity.CameraPos + playerEntity.LocalEyePos; - double dx = cameraPos.X - (blockPos.X + 0.5d); - double dz = cameraPos.Z - (blockPos.Z + 0.5d); - if (Math.Abs(dx) <= 0.0001d && Math.Abs(dz) <= 0.0001d) - return false; - - double absDx = Math.Abs(dx); - double absDz = Math.Abs(dz); - double diagonalTolerance = Math.Max(absDx, absDz) * 0.65d; - bool isDiagonalView = Math.Abs(absDx - absDz) <= diagonalTolerance; - if (isDiagonalView) - { - Vec3f facingX = dx >= 0d ? new Vec3f(1f, 0f, 0f) : new Vec3f(-1f, 0f, 0f); - Vec3f facingZ = dz >= 0d ? new Vec3f(0f, 0f, 1f) : new Vec3f(0f, 0f, -1f); - - int obstructionX = GetFacingObstructionScore(blockPos, facingX); - int obstructionZ = GetFacingObstructionScore(blockPos, facingZ); - if (obstructionX != obstructionZ) - { - facing = obstructionX < obstructionZ ? facingX : facingZ; - return true; - } - } - - facing = absDx >= absDz - ? (dx >= 0d ? new Vec3f(1f, 0f, 0f) : new Vec3f(-1f, 0f, 0f)) - : (dz >= 0d ? new Vec3f(0f, 0f, 1f) : new Vec3f(0f, 0f, -1f)); - - return true; - } - - private int GetFacingObstructionScore(BlockPos blockPos, Vec3f facing) - { - int offsetX = Math.Sign(facing.X); - int offsetZ = Math.Sign(facing.Z); - int score = 0; - - for (int yOffset = 0; yOffset <= 1; yOffset++) - { - BlockPos checkPos = new(blockPos.X + offsetX, blockPos.Y + yOffset, blockPos.Z + offsetZ); - if (IsSolidBlock(checkPos)) - score++; - } - - return score; - } - - private bool IsSolidBlock(BlockPos blockPos) - { - Block block = api.World.BlockAccessor.GetBlock(blockPos); - if (block.Id == 0) - return false; - - Cuboidf[]? collisionBoxes = block.GetCollisionBoxes(api.World.BlockAccessor, blockPos); - return collisionBoxes is { Length: > 0 }; - } - - private static bool TryGetBlockSideFacing(Block block, out Vec3f facing) - { - facing = new Vec3f(0f, 0f, 1f); - - string? side = block.Variant?["side"]; - if (string.IsNullOrWhiteSpace(side)) - return false; - - switch (side.ToLowerInvariant()) - { - case "north": - facing = new Vec3f(0f, 0f, 1f); - return true; - case "south": - facing = new Vec3f(0f, 0f, -1f); - return true; - case "east": - facing = new Vec3f(-1f, 0f, 0f); - return true; - case "west": - facing = new Vec3f(1f, 0f, 0f); - return true; - case "up": - facing = new Vec3f(0f, 1f, 0f); - return true; - case "down": - facing = new Vec3f(0f, -1f, 0f); - return true; - default: - return false; - } - } - - private static void BuildBasisFromForward(Vec3f forward, out Vec3f right, out Vec3f up) - { - up = new Vec3f(0f, 1f, 0f); - - if (Math.Abs(forward.Y) > 0.5f) - { - up = forward.Y > 0f ? new Vec3f(0f, 0f, -1f) : new Vec3f(0f, 0f, 1f); - right = new Vec3f(1f, 0f, 0f); - return; - } - - right = new Vec3f(-forward.Z, 0f, forward.X); - } - - private static void NormalizeFacing(ref Vec3f facing) - { - float length = MathF.Sqrt(facing.X * facing.X + facing.Y * facing.Y + facing.Z * facing.Z); - if (length <= 0.0001f) - { - facing = new Vec3f(0f, 0f, 1f); - return; - } - - float invLength = 1f / length; - facing = new Vec3f(facing.X * invLength, facing.Y * invLength, facing.Z * invLength); - } - - private static bool TryGetMeshAngle(BlockEntity blockEntity, out float radians) - { - if (blockEntity is BlockEntityGenericTypedContainer genericContainer) - { - radians = genericContainer.MeshAngle; - return true; - } - - if (blockEntity is BlockEntityCrate crate) - { - radians = crate.MeshAngle; - return true; - } - - radians = 0f; - return false; - } -} +using ChestPreview.Configs; +using ChestPreview.Models; +using ChestPreview.Utils; +using System; +using System.Collections.Generic; +using System.Numerics; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; +using Vintagestory.GameContent; + +namespace ChestPreview.Rendering; + +internal class WorldBillboardPresenter(ICoreClientAPI api) +{ + private const int multiblock_scan_radius = 2; + private static readonly float front_face_offset = 0.05f; + + private readonly ICoreClientAPI api = api; + private readonly List frameBillboards = []; + + public IReadOnlyList FrameBillboards => frameBillboards; + + public void PrepareFrame(List previewTargets) + { + frameBillboards.Clear(); + + foreach (PreviewTarget previewTarget in previewTargets) + { + if (!CanPresentAsBillboard(previewTarget)) + continue; + + if (!TryCreateBillboardTarget(previewTarget, out BillboardTarget billboardTarget)) + continue; + + frameBillboards.Add(billboardTarget); + } + } + + public void Clear() + { + frameBillboards.Clear(); + } + + private static bool CanPresentAsBillboard(PreviewTarget previewTarget) + { + return previewTarget.Mode is PreviewModes.ON_HOVERED_CONTAINER or PreviewModes.ON_NEARBY_CONTAINERS; + } + + private bool TryCreateBillboardTarget(PreviewTarget previewTarget, out BillboardTarget billboardTarget) + { + return TryCreateFrontBillboardTarget(previewTarget, out billboardTarget); + } + + private bool TryCreateFrontBillboardTarget(PreviewTarget previewTarget, out BillboardTarget billboardTarget) + { + billboardTarget = default; + + Block block = previewTarget.Block; + BlockPos blockPos = previewTarget.BlockEntity.Pos; + GetContainerBounds(block, blockPos, out Vector3 min, out Vector3 max); + + if (!TryResolveFacingNormal(previewTarget.Block, previewTarget.BlockEntity, out Vec3f forward, out Vec3f right, out Vec3f up)) + return false; + + Vector3 centerLocal = (min + max) * 0.5f; + Vector3 halfExtents = (max - min) * 0.5f; + Vector3 rightVector = right.ToVector3(); + Vector3 upVector = up.ToVector3(); + Vector3 forwardVector = forward.ToVector3(); + + float halfWidth = Vector3.Dot(Vector3.Abs(rightVector), halfExtents); + float halfHeight = Vector3.Dot(Vector3.Abs(upVector), halfExtents); + float halfDepth = ResolveStableHalfDepth(forwardVector, halfExtents); + + float width = halfWidth * 2f; + float height = halfHeight * 2f; + + Vector3 center = blockPos.ToVector3() + centerLocal + forwardVector * (halfDepth + front_face_offset); + + width = Math.Max(0.05f, width); + height = Math.Max(0.05f, height); + + billboardTarget = new BillboardTarget(previewTarget, center.ToVec3d(), right, up, forward, width, height, true); + return true; + } + + private static float ResolveStableHalfDepth(Vector3 forward, Vector3 halfExtents) + { + if (MathF.Abs(forward.Y) < 0.5f) + return MathF.Abs(forward.X) >= MathF.Abs(forward.Z) ? halfExtents.X : halfExtents.Z; + + return MathF.Abs(forward.Y) >= MathF.Abs(forward.Z) ? halfExtents.Y : halfExtents.Z; + } + + private void GetContainerBounds(Block block, BlockPos blockPos, out Vector3 min, out Vector3 max) + { + min = new Vector3(0f, 0f, 0f); + max = new Vector3(1f, 1f, 1f); + + ExpandBoundsFromSelectionBoxes(block, blockPos, blockPos, ref min, ref max); + ExpandBoundsFromLinkedMultiblockParts(blockPos, ref min, ref max); + } + + private void ExpandBoundsFromLinkedMultiblockParts(BlockPos controllerPos, ref Vector3 min, ref Vector3 max) + { + int minScanX = controllerPos.X - multiblock_scan_radius; + int maxScanX = controllerPos.X + multiblock_scan_radius; + int minScanY = controllerPos.Y - multiblock_scan_radius; + int maxScanY = controllerPos.Y + multiblock_scan_radius; + int minScanZ = controllerPos.Z - multiblock_scan_radius; + int maxScanZ = controllerPos.Z + multiblock_scan_radius; + Vector3 minLocal = min; + Vector3 maxLocal = max; + + api.World.BlockAccessor.WalkBlocks(new BlockPos(minScanX, minScanY, minScanZ), new BlockPos(maxScanX, maxScanY, maxScanZ), (partBlock, x, y, z) => + { + if (x == controllerPos.X && y == controllerPos.Y && z == controllerPos.Z) + return; + + if (partBlock.Id == 0 || partBlock is not IMultiblockOffset multiblockOffset) + return; + + BlockPos partPos = new(x, y, z); + BlockPos linkedControllerPos = multiblockOffset.GetControlBlockPos(partPos); + if (!IsSameBlockPos(linkedControllerPos, controllerPos)) + return; + + ExpandBoundsFromSelectionBoxes(partBlock, partPos, controllerPos, ref minLocal, ref maxLocal); + }); + + min = minLocal; + max = maxLocal; + } + + private void ExpandBoundsFromSelectionBoxes(Block block, BlockPos sourcePos, BlockPos originPos, ref Vector3 min, ref Vector3 max) + { + Cuboidf[]? selectionBoxes = block.GetSelectionBoxes(api.World.BlockAccessor, sourcePos); + Vector3 sourceOffset = sourcePos.ToVector3() - originPos.ToVector3(); + if (selectionBoxes is not { Length: > 0 }) + { + min = Vector3.Min(min, sourceOffset); + max = Vector3.Max(max, sourceOffset + Vector3.One); + return; + } + + foreach (Cuboidf selectionBox in selectionBoxes) + { + min = Vector3.Min(min, sourceOffset + selectionBox.Start.ToVector3()); + max = Vector3.Max(max, sourceOffset + selectionBox.End.ToVector3()); + } + } + + private static bool IsSameBlockPos(BlockPos a, BlockPos b) + { + return a.X == b.X && a.Y == b.Y && a.Z == b.Z; + } + + private bool TryResolveFacingNormal(Block block, BlockEntity blockEntity, out Vec3f forward, out Vec3f right, out Vec3f up) + { + forward = new Vec3f(0f, 0f, 1f); + right = new Vec3f(1f, 0f, 0f); + up = new Vec3f(0f, 1f, 0f); + + if (TryGetMeshAngle(blockEntity, out float meshAngleRadians)) + { + // MeshAngle is around Y axis in radians. + forward = new Vec3f(MathF.Sin(meshAngleRadians), 0f, MathF.Cos(meshAngleRadians)); + NormalizeFacing(ref forward); + BuildBasisFromForward(forward, out right, out up); + return true; + } + + if (TryGetBlockSideFacing(block, out forward)) + { + BuildBasisFromForward(forward, out right, out up); + return true; + } + + if (!TryGetPlayerFacing(blockEntity.Pos, out forward)) + return false; + + BuildBasisFromForward(forward, out right, out up); + return true; + } + + private bool TryGetPlayerFacing(BlockPos blockPos, out Vec3f facing) + { + facing = new Vec3f(0f, 0f, 1f); + + EntityPlayer? playerEntity = api.World.Player?.Entity; + if (playerEntity?.CameraPos == null) + return false; + + Vec3d cameraPos = playerEntity.CameraPos + playerEntity.LocalEyePos; + double dx = cameraPos.X - (blockPos.X + 0.5d); + double dz = cameraPos.Z - (blockPos.Z + 0.5d); + if (Math.Abs(dx) <= 0.0001d && Math.Abs(dz) <= 0.0001d) + return false; + + double absDx = Math.Abs(dx); + double absDz = Math.Abs(dz); + double diagonalTolerance = Math.Max(absDx, absDz) * 0.65d; + bool isDiagonalView = Math.Abs(absDx - absDz) <= diagonalTolerance; + if (isDiagonalView) + { + Vec3f facingX = dx >= 0d ? new Vec3f(1f, 0f, 0f) : new Vec3f(-1f, 0f, 0f); + Vec3f facingZ = dz >= 0d ? new Vec3f(0f, 0f, 1f) : new Vec3f(0f, 0f, -1f); + + int obstructionX = GetFacingObstructionScore(blockPos, facingX); + int obstructionZ = GetFacingObstructionScore(blockPos, facingZ); + if (obstructionX != obstructionZ) + { + facing = obstructionX < obstructionZ ? facingX : facingZ; + return true; + } + } + + facing = absDx >= absDz + ? (dx >= 0d ? new Vec3f(1f, 0f, 0f) : new Vec3f(-1f, 0f, 0f)) + : (dz >= 0d ? new Vec3f(0f, 0f, 1f) : new Vec3f(0f, 0f, -1f)); + + return true; + } + + private int GetFacingObstructionScore(BlockPos blockPos, Vec3f facing) + { + int offsetX = Math.Sign(facing.X); + int offsetZ = Math.Sign(facing.Z); + int score = 0; + + for (int yOffset = 0; yOffset <= 1; yOffset++) + { + BlockPos checkPos = new(blockPos.X + offsetX, blockPos.Y + yOffset, blockPos.Z + offsetZ); + if (IsSolidBlock(checkPos)) + score++; + } + + return score; + } + + private bool IsSolidBlock(BlockPos blockPos) + { + Block block = api.World.BlockAccessor.GetBlock(blockPos); + if (block.Id == 0) + return false; + + Cuboidf[]? collisionBoxes = block.GetCollisionBoxes(api.World.BlockAccessor, blockPos); + return collisionBoxes is { Length: > 0 }; + } + + private static bool TryGetBlockSideFacing(Block block, out Vec3f facing) + { + facing = new Vec3f(0f, 0f, 1f); + + string? side = block.Variant?["side"]; + if (string.IsNullOrWhiteSpace(side)) + return false; + + switch (side.ToLowerInvariant()) + { + case "north": + facing = new Vec3f(0f, 0f, 1f); + return true; + case "south": + facing = new Vec3f(0f, 0f, -1f); + return true; + case "east": + facing = new Vec3f(-1f, 0f, 0f); + return true; + case "west": + facing = new Vec3f(1f, 0f, 0f); + return true; + case "up": + facing = new Vec3f(0f, 1f, 0f); + return true; + case "down": + facing = new Vec3f(0f, -1f, 0f); + return true; + default: + return false; + } + } + + private static void BuildBasisFromForward(Vec3f forward, out Vec3f right, out Vec3f up) + { + up = new Vec3f(0f, 1f, 0f); + + if (Math.Abs(forward.Y) > 0.5f) + { + up = forward.Y > 0f ? new Vec3f(0f, 0f, -1f) : new Vec3f(0f, 0f, 1f); + right = new Vec3f(1f, 0f, 0f); + return; + } + + right = new Vec3f(-forward.Z, 0f, forward.X); + } + + private static void NormalizeFacing(ref Vec3f facing) + { + float length = MathF.Sqrt(facing.X * facing.X + facing.Y * facing.Y + facing.Z * facing.Z); + if (length <= 0.0001f) + { + facing = new Vec3f(0f, 0f, 1f); + return; + } + + float invLength = 1f / length; + facing = new Vec3f(facing.X * invLength, facing.Y * invLength, facing.Z * invLength); + } + + private static bool TryGetMeshAngle(BlockEntity blockEntity, out float radians) + { + if (blockEntity is BlockEntityGenericTypedContainer genericContainer) + { + radians = genericContainer.MeshAngle; + return true; + } + + if (blockEntity is BlockEntityCrate crate) + { + radians = crate.MeshAngle; + return true; + } + + radians = 0f; + return false; + } +}