Use one cached bounding box per sculpture (beta.124)
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
+24
-13
@@ -29,7 +29,7 @@ import net.minecraft.world.level.storage.TagValueInput;
|
||||
import net.minecraft.world.phys.*;
|
||||
import net.minecraft.world.phys.shapes.*;
|
||||
|
||||
/** Native picking/collision and light propagation, compared with the actual emitted sculpture mesh. */
|
||||
/** Single-box selection/collision bounds and light propagation compared with the real sculpture mesh. */
|
||||
public final class Sculpture122ClientChecks implements FabricClientGameTest {
|
||||
private static final String MODEL="sanctuary:model/"+"1".repeat(64);
|
||||
private static final BlockPos TARGET=new BlockPos(0,1,0),SKY=new BlockPos(10,2,2),CONTROL=new BlockPos(18,2,2);
|
||||
@@ -60,12 +60,12 @@ public final class Sculpture122ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m->{physical(m.level,TARGET);geometry(m);Sculpture113ClientChecks.geometry(m,MODEL);});
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.0625,1.5,.5)));c.waitTicks(5);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET),"Native cursor selects an occupied pillar"));
|
||||
c.takeScreenshot("sculpture122-voxel-outline");
|
||||
c.takeScreenshot("sculpture124-single-outline");
|
||||
server.runOnServer(s->player(s).level().setBlockAndUpdate(TARGET.north(),Blocks.REDSTONE_BLOCK.defaultBlockState()));
|
||||
c.waitFor(m->m.level.getBlockState(TARGET.north()).is(Blocks.REDSTONE_BLOCK),100);
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,1.5,.5)));c.waitTicks(5);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET.north()),"Native cursor passes through the sculpture hole to the block behind"));
|
||||
c.takeScreenshot("sculpture122-pick-through-hole");
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET),"Native cursor selects the sculpture bounds even through its visual hole"));
|
||||
c.takeScreenshot("sculpture124-select-volume");
|
||||
// A state edit and fresh entity payload must invalidate geometry on both sides.
|
||||
server.runOnServer(s->{var l=player(s).level();l.setBlockAndUpdate(TARGET,l.getBlockState(TARGET).setValue(Statuary.FACING,Direction.WEST).setValue(Statuary.ANCHOR,SculptureAnchor.EAST));});
|
||||
c.waitFor(m->m.level.getBlockState(TARGET).getValue(Statuary.FACING)==Direction.WEST,100);
|
||||
@@ -97,7 +97,7 @@ public final class Sculpture122ClientChecks implements FabricClientGameTest {
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&ARCH.equals(e.sculpture()),1200);
|
||||
world.getServer().runOnServer(s->physical(player(s).level(),TARGET));c.runOnClient(m->physical(m.level,TARGET));
|
||||
c.waitFor(m->m.level.getBrightness(LightLayer.SKY,SKY)==15,200);
|
||||
System.out.println("SCULPTURE122_PASS: native placement, voxel outline and picking through holes, authoritative collisions, shared render geometry, cache refresh, light propagation and save/reconnect");
|
||||
System.out.println("SCULPTURE122_PASS: native placement, one-box outline including holes, single-box physical collisions, shared render geometry, cache refresh, light propagation and save/reconnect");
|
||||
}
|
||||
}
|
||||
private static void placementOverlap(ServerPlayer player){
|
||||
@@ -107,20 +107,25 @@ public final class Sculpture122ClientChecks implements FabricClientGameTest {
|
||||
var hit=new BlockHitResult(new Vec3(3.01,1,.5),Direction.UP,pos.below(),false);
|
||||
var blocked=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
|
||||
check(!blocked.consumesAction()&&level.getBlockState(pos).isAir()&&stack.getCount()==1,"Native placement refuses occupied voxels inside player without consuming sculpture");
|
||||
player.teleportTo(3.5,1,.5);var pillars=new ArrayList<VoxelSculpture.Cell>();for(int y=0;y<16;y++)for(int x:new int[]{0,15})pillars.add(new VoxelSculpture.Cell(x,y,0,-1));
|
||||
var hollow=new VoxelSculpture(MODEL,"Two pillars",16,16,1,pillars);stack=StatuaryEntity.item(hollow);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
|
||||
var hollowBlocked=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
|
||||
check(!hollowBlocked.consumesAction()&&level.getBlockState(pos).isAir(),"Placement also rejects a player inside the enclosing box between separated pillars");player.teleportTo(3.75,1,.5);
|
||||
var postCells=new ArrayList<VoxelSculpture.Cell>();for(int y=0;y<16;y++)for(int x=0;x<2;x++)for(int z=0;z<2;z++)postCells.add(new VoxelSculpture.Cell(x,y,z,-1));
|
||||
var post=new VoxelSculpture(MODEL,"Post",2,16,2,postCells);stack=StatuaryEntity.item(post);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
|
||||
var allowed=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
|
||||
check(allowed.consumesAction()&&level.getBlockState(pos).is(Statuary.BLOCK),"Native placement allows empty part of block beside player");
|
||||
check(level.noCollision(player,player.getBoundingBox()),"Accepted sculpture does not intersect player");
|
||||
level.setBlockAndUpdate(pos,Blocks.AIR.defaultBlockState());player.teleportTo(.5,1,3.2);player.getInventory().setItem(player.getInventory().getSelectedSlot(),StatuaryEntity.item(ARCH));player.inventoryMenu.broadcastFullState();
|
||||
System.out.println("SCULPTURE122_PLACEMENT_PASS native item rejects occupied voxels, allows empty space, preserves stack on rejection");
|
||||
System.out.println("SCULPTURE122_PLACEMENT_PASS native item rejects occupied bounds including visual holes, allows space outside the box, preserves stack on rejection");
|
||||
}
|
||||
private static void physical(Level level,BlockPos pos){
|
||||
var state=level.getBlockState(pos);var shape=state.getShape(level,pos);var collision=state.getCollisionShape(level,pos);
|
||||
check(shape==collision,"Selection and native collision share cached shape");check(shape.bounds().getZsize()==.25,"No invisible full-block depth");
|
||||
check(level.clip(new ClipContext(new Vec3(.5,1.5,2),new Vec3(.5,1.5,-.1),ClipContext.Block.OUTLINE,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.MISS,"Outline ray passes through empty arch center");
|
||||
check(shape==collision&&shape.toAabbs().size()==1,"Selection and collision share one cached bounding box");check(shape==state.getShape(level,pos),"Selection queries reuse the cached box");check(shape.bounds().getZsize()==.25,"No invisible full-block depth");
|
||||
check(level.clip(new ClipContext(new Vec3(.5,1.5,2),new Vec3(.5,1.5,-.1),ClipContext.Block.OUTLINE,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Outline ray selects the whole sculpture volume");
|
||||
check(level.clip(new ClipContext(new Vec3(.0625,1.5,2),new Vec3(.0625,1.5,-.1),ClipContext.Block.COLLIDER,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Collision ray hits pillar");
|
||||
check(level.noCollision(new AABB(.35,1.2,.35,.65,1.7,.65)),"Native collision query leaves hole empty");
|
||||
check(level.clip(new ClipContext(new Vec3(.5,1.5,2),new Vec3(.5,1.5,-.1),ClipContext.Block.COLLIDER,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Physical collision uses the enclosing volume, including visual holes");
|
||||
check(!level.noCollision(new AABB(.35,1.2,.35,.65,1.7,.65)),"Native collision query fills the inner volume instead of following tiny details");
|
||||
check(!level.noCollision(new AABB(.02,1.2,.4,.1,1.7,.6)),"Native collision query blocks occupied pillar");
|
||||
check(!state.isCollisionShapeFullBlock(level,pos)&&!state.isSolidRender()&&!state.canOcclude(),"Sculpture is not a solid occluding cube");
|
||||
check(state.getOcclusionShape().isEmpty()&&state.propagatesSkylightDown()&&state.getLightDampening()==0&&state.getShadeBrightness(level,pos)==1,"Light metadata never reintroduces full-block occlusion");
|
||||
@@ -132,13 +137,19 @@ public final class Sculpture122ClientChecks implements FabricClientGameTest {
|
||||
var models=List.of(ARCH,padded,new VoxelSculpture(MODEL,"Dense",16,16,16,dense),new VoxelSculpture(MODEL,"One voxel",1,1,1,List.of(new VoxelSculpture.Cell(0,0,0,-1))));
|
||||
int cases=0;for(var model:models)for(var direction:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values()){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,direction).setValue(Statuary.ANCHOR,anchor);var entity=new StatuaryEntity(BlockPos.ZERO,state);entity.setLevel(m.level);load(entity,model,m.level);
|
||||
var shape=entity.shape(state);check(shape==entity.shape(state),"Repeated geometry queries reuse the same shape");var boxes=shape.toAabbs();double volume=boxes.stream().mapToDouble(b->b.getXsize()*b.getYsize()*b.getZsize()).sum();check(volume==model.cells().size()/4096d,"Shape contains exactly the occupied volume");
|
||||
var shape=entity.shape(state);check(shape==entity.shape(state),"Repeated geometry queries reuse the same shape");var boxes=shape.toAabbs();double volume=boxes.stream().mapToDouble(b->b.getXsize()*b.getYsize()*b.getZsize()).sum();check(boxes.size()==1&&volume>=model.cells().size()/4096d,"One box encloses all occupied cells");
|
||||
var outline=entity.shape(state);check(outline==entity.shape(state),"Repeated outline queries reuse the cached box");
|
||||
check(outline.toAabbs().size()==1&&outline.bounds().equals(shape.bounds()),"One outline box matches exact rendered extents for every orientation and anchor");
|
||||
int[] edges={0};outline.forAllEdges((x1,y1,z1,x2,y2,z2)->edges[0]++);check(edges[0]==12,"Native outline emits only the 12 enclosing edges, independent of voxel count");
|
||||
var renderer=new StatuaryRenderer();var render=renderer.createRenderState();renderer.extractRenderState(entity,render,0,Vec3.ZERO,null);
|
||||
for(var q:render.mesh){var center=new Vec3(q.x()+1/32d,q.y()+1/32d,q.z()+1/32d);check(boxes.stream().anyMatch(b->b.contains(center)),"Every rendered cell is physically present");
|
||||
var normal=switch(q.side()){case 0->Direction.NORTH;case 1->Direction.SOUTH;case 2->Direction.WEST;case 3->Direction.EAST;case 4->Direction.UP;default->Direction.DOWN;};var neighbor=center.add(normal.getStepX()/16d,normal.getStepY()/16d,normal.getStepZ()/16d);check(boxes.stream().noneMatch(b->b.contains(neighbor)),"Rendered exposed face is also exposed physically");}
|
||||
double minX=1,minY=1,minZ=1,maxX=0,maxY=0,maxZ=0;
|
||||
for(var q:render.mesh){minX=Math.min(minX,q.x());minY=Math.min(minY,q.y());minZ=Math.min(minZ,q.z());maxX=Math.max(maxX,q.x()+1/16d);maxY=Math.max(maxY,q.y()+1/16d);maxZ=Math.max(maxZ,q.z()+1/16d);var center=new Vec3(q.x()+1/32d,q.y()+1/32d,q.z()+1/32d);check(boxes.stream().anyMatch(b->b.contains(center)),"Every rendered cell is physically present");
|
||||
}
|
||||
check(shape.bounds().equals(new AABB(minX,minY,minZ,maxX,maxY,maxZ)),"Single collision box exactly matches visible mesh extents");
|
||||
cases++;
|
||||
}
|
||||
System.out.println("SCULPTURE122_GEOMETRY_PASS "+cases+" native voxel shapes agree with emitted meshes, including holes, padding, dense 4096 cells, single voxel, 4 directions and 5 anchors");
|
||||
System.out.println("SCULPTURE124_BOUNDS_PASS "+cases+" cached single boxes agree with exact renderer extents; 12 outline edges each");
|
||||
System.out.println("SCULPTURE122_GEOMETRY_PASS "+cases+" native bounding boxes agree with emitted mesh extents, plus cached 12-edge selection bounds for holes, padding, dense 4096 cells, single voxel, 4 directions and 5 anchors");
|
||||
}
|
||||
private static void place(ServerLevel level,BlockPos pos,VoxelSculpture model,Direction facing,SculptureAnchor anchor){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor);level.setBlockAndUpdate(pos,state);var entity=(StatuaryEntity)level.getBlockEntity(pos);load(entity,model,level);entity.setChanged();level.sendBlockUpdated(pos,state,state,3);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package fr.koka.sanctuary.plans;
|
||||
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape;
|
||||
import net.minecraft.world.phys.shapes.CubeVoxelShape;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/** One integer placement shared by rendering, picking and server-side collisions. */
|
||||
/** Integer placement shared by rendering and tight, single-box selection/collision bounds. */
|
||||
public final class SculptureGeometry {
|
||||
public record Placement(VoxelSculpture sculpture, int offsetX, int offsetZ) {}
|
||||
|
||||
@@ -23,13 +22,16 @@ public final class SculptureGeometry {
|
||||
anchor.originZ(maxZ - minZ) - minZ);
|
||||
}
|
||||
|
||||
/** Tight integer bounds, without rotating/copying cells or constructing a voxel grid. */
|
||||
public static VoxelShape shape(VoxelSculpture sculpture, Direction facing, SculptureAnchor anchor) {
|
||||
var placed = place(sculpture, facing, anchor);
|
||||
var grid = new BitSetDiscreteVoxelShape(16, 16, 16);
|
||||
for (var cell : placed.sculpture().cells()) {
|
||||
grid.fill(cell.x() + placed.offsetX(), cell.y(), cell.z() + placed.offsetZ());
|
||||
int minX=16,minY=16,minZ=16,maxX=0,maxY=0,maxZ=0;
|
||||
for(var cell:sculpture.cells()) {
|
||||
minX=Math.min(minX,cell.x()); minY=Math.min(minY,cell.y()); minZ=Math.min(minZ,cell.z());
|
||||
maxX=Math.max(maxX,cell.x()+1); maxY=Math.max(maxY,cell.y()+1); maxZ=Math.max(maxZ,cell.z()+1);
|
||||
}
|
||||
// Build the bounded native voxel grid directly, without thousands of box unions.
|
||||
return new CubeVoxelShape(grid);
|
||||
int width=maxX-minX,depth=maxZ-minZ;
|
||||
if(facing.getAxis()==Direction.Axis.X){int swap=width;width=depth;depth=swap;}
|
||||
int x=anchor.originX(width),z=anchor.originZ(depth);
|
||||
return Block.box(x,minY,z,x+width,maxY,z+depth);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user