Hytale Raycasting and Hit Detection: The Geometry of Interaction
A source-grounded tour of Hytale's raycasting and hit-detection math: ray-vs-AABB intersection, DDA voxel traversal for block targeting, slab-based entity ray tests, and the frustum-projection executor that powers melee selectors.
Every interaction in Hytale that involves "what is the player looking at" or "what did this swing connect with" reduces to a small amount of carefully written geometry. Breaking a block, picking a target NPC, checking whether a guard can see you, and resolving a melee stab all run through the same family of math primitives in the com.hypixel.hytale.math package and its consumers in the server core. This reference is based on Hytale server version 2026.03.26-89796e57b, read directly from the decompiled server.
The codebase splits the work into three layers. At the bottom sit pure geometric kernels: RaycastAABB and CollisionMath resolve a ray against an axis-aligned box, while BlockIterator walks the voxel grid a ray passes through. Above them, server utilities like TargetUtil and RayBlockHitTest turn those kernels into "what block / entity am I aiming at." At the top, the combat interaction system uses a HitDetectionExecutor that projects geometry through a camera-style frustum to decide which entities a weapon swing actually selects. We will work bottom-up.
#Ray vs. AABB: the slab method in two flavors
The foundational operation is intersecting a ray with an axis-aligned bounding box (AABB). Hytale ships two independent implementations, and the difference between them is instructive.
RaycastAABB.intersect(...) tests the ray against all six faces of the box and returns the nearest hit parameter tNear (or Double.POSITIVE_INFINITY for a miss). It is written in terms of raw doubles — box minX..maxZ, ray origin ox, oy, oz, and direction dx, dy, dz — so it allocates nothing:
// RaycastAABB.java — nearest-face intersection, per axis
double t = (minX - ox) / dx; // parameter where ray crosses the -X face
if (t < tNear && t > -1.0E-8) { // EPSILON guards against grazing/behind hits
double u = oz + dz * t; // project onto the other two axes
double v = oy + dy * t;
if (u >= minZ && u <= maxZ && v >= minY && v <= maxY) {
tNear = t; // the (u,v) point lands on the face → real hit
}
}The class exposes a callback variant that additionally reports the surface normal of the face that was struck, via the RaycastConsumer functional interface:
consumer.accept(tNear != Double.POSITIVE_INFINITY, // hit?
ox, oy, oz, dx, dy, dz, // the ray
tNear, // distance parameter
nx, ny, nz); // face normal, e.g. (-1, 0, 0)Three overloads — RaycastConsumerPlus1, RaycastConsumerPlus2, and RaycastConsumerPlus3 — thread one, two, or three extra context objects through the call so you can avoid capturing lambdas in a hot loop. The EPSILON constant (-1.0E-8) is the only fudge factor; everything else is exact.
The second implementation lives in CollisionMath and uses the classic slab method, decomposing the 3D test into three independent 1D interval clips:
// CollisionMath.intersectRayAABB — slab test against a Box at world (x,y,z)
minMax.x = 0.0; // entry parameter t_min
minMax.y = Double.MAX_VALUE; // exit parameter t_max
Vector3d min = box.getMin();
Vector3d max = box.getMax();
return intersect1D(pos.x, ray.getX(), x + min.x, x + max.x, minMax)
&& intersect1D(pos.y, ray.getY(), y + min.y, y + max.y, minMax)
&& intersect1D(pos.z, ray.getZ(), z + min.z, z + max.z, minMax)
&& minMax.x >= 0.0; // reject boxes entirely behind the originintersect1D clamps the running [t_min, t_max] interval against one slab and returns false the moment the interval becomes empty (minMax.x <= minMax.y fails). When the ray is parallel to a slab (Math.abs(s) < 1.0E-5) it degenerates to a simple containment check on that axis. The companion intersectVectorAABB reuses the same code but adds minMax.x <= 1.0, which treats the direction as a finite segment rather than an infinite ray — the distinction between "is there a wall anywhere ahead" and "did this exact displacement reach the wall."
| Method | Returns | Reports normal | Segment-bounded |
|---|---|---|---|
RaycastAABB.intersect | nearest t (double) | no | no (infinite ray) |
RaycastAABB.intersect(..., RaycastConsumer) | via callback | yes (nx, ny, nz) | no |
CollisionMath.intersectRayAABB | boolean + minMax | no | no |
CollisionMath.intersectVectorAABB | boolean + minMax | no | yes (t ≤ 1) |
Why two ray-AABB routines?
RaycastAABB is the lower-level, allocation-free kernel that also recovers the hit normal — useful when you need to know which face was struck (for block placement, decals, or knockback direction). CollisionMath returns the entry/exit interval, which is exactly what swept collision and segment queries need. Pick the one whose output you actually consume.
#Walking the voxel grid: BlockIterator and DDA
A ray that travels tens of blocks should not test every box in the world. For block targeting, Hytale uses BlockIterator, a digital-differential-analyzer (DDA) traversal that visits exactly the voxels a ray passes through, in order, from origin outward.
The entry points are iterate(origin, direction, maxDistance, procedure) and iterateFromTo(from, to, procedure). Internally iterate0 floors the origin to a starting block, then repeatedly calls a private intersection helper that computes the parametric distance t to the next voxel boundary along whichever axis the ray exits first:
// BlockIterator.iterate0 — advance one voxel per loop
int bx = (int) FastMath.fastFloor(sx), by = ..., bz = ...; // current block
double px = sx - bx, py = ..., pz = ...; // position within block [0,1)
while (pt <= maxDistance) {
double t = intersection(px, py, pz, dx, dy, dz); // distance to next face
double qx = px + t * dx, qy = ..., qz = ...; // exit point on the boundary
if (!procedure.accept(bx, by, bz, px, py, pz, qx, qy, qz)) {
return false; // caller hit something → stop
}
// step the block coordinate across whichever face we exited
if (dx > 0.0 && FastMath.gEq(qx, 1.0)) { qx--; bx++; }
else if (dx < 0.0 && FastMath.sEq(qx, 0.0)) { qx++; bx--; }
// ...same for y and z...
pt += t; px = qx; py = qy; pz = qz;
}The BlockIteratorProcedure.accept callback returns boolean: true to keep marching, false to terminate. That single return value is how every block-ray query expresses "stop, I found my hit." The traversal validates its inputs up front — checkParameters rejects NaN, infinite, or zero-length directions — so a malformed look vector throws an IllegalArgumentException instead of looping forever.
The return-false contract
Your procedure returning false does double duty: it stops the walk and it is the signal the outer query inspects. In TargetUtil.getTargetBlock, the iterate call's boolean result is inverted — success (ran to completion, hit nothing) yields null, while early termination yields the block you stopped on. Get this inversion backwards and your raycast will report empty air as a hit.
#Block targeting in practice: TargetUtil and RayBlockHitTest
TargetUtil is the server's general-purpose "what block is this entity aiming at" facade. getTargetBlock builds a BlockIterator over the look vector and, inside the procedure, fetches the block ID at each visited voxel from the chunk's BlockSection, testing it against a caller-supplied predicate:
// TargetUtil.getTargetBlock — stop on the first block the predicate accepts
boolean success = BlockIterator.iterate(
originX, originY, originZ, directionX, directionY, directionZ, maxDistance,
(x, y, z, px, py, pz, qx, qy, qz, buffer) -> {
if (y < 0 || y >= 320) return false; // out of world height → stop
buffer.updateChunk(x, z); // lazily swap chunk on crossing
int blockId = buffer.currentBlockChunk.getSectionAtBlockY(y).get(x, y, z);
int fluidId = WorldUtil.getFluidIdAtPosition(/* ... */);
return !blockIdPredicate.test(blockId, fluidId); // matched → return false → stop
}, buffer);
return success ? null : new Vector3i(buffer.x, buffer.y, buffer.z);The TargetBuffer cached in the closure is the performance trick: chunk lookups are expensive, so it only re-resolves the BlockChunk when the ray crosses a chunk boundary (updateChunk compares against the last currentChunkX/Z). The variant getTargetLocation returns a precise Vector3d (block coordinate plus the in-voxel offset px, py, pz) instead of a block coordinate, which is what you want for spawn points and particle anchors rather than block edits.
RayBlockHitTest shows the same pattern specialized for NPC AI. It implements BlockIterator.BlockIteratorProcedure directly (rather than via lambda) and is pooled in a ThreadLocal to avoid per-tick allocation. Its accept treats block ID 0 as air (keep going), block ID 1 as an opaque blocker (stop, no hit), and any other ID as a candidate filtered through a BlockSetModule membership check. Its init method seeds the ray from the entity's eye height — origin.y += modelComponent.getModel().getEyeHeight() — and builds the direction from head yaw and pitch.
That direction construction is worth isolating, because it is shared everywhere. Vector3d.assign(double yaw, double pitch) converts an entity's look angles into a unit direction vector:
// Vector3d.assign(yaw, pitch) — angles → direction
double len = TrigMathUtil.cos(pitch);
double x = len * -TrigMathUtil.sin(yaw);
double y = TrigMathUtil.sin(pitch);
double z = len * -TrigMathUtil.cos(yaw);TargetUtil.getLook wraps this up, assembling a Transform from the entity's TransformComponent position (offset by eye height) and HeadRotation, exposing getPosition() and getDirection() as the canonical ray for any aiming query.
#Entity hit tests: rays against bounding boxes
Targeting an entity is the dual problem — instead of marching the grid, you collect candidates and ray-test each one's box. TargetUtil.getTargetEntity gathers everything in an 8-block sphere via getAllEntitiesInSphere, then filters with isHitByRay:
// TargetUtil.isHitByRay — ray vs. the entity's world-placed AABB
Box boundingBox = boundingBoxComponent.getBoundingBox();
Vector3d position = transformComponent.getPosition();
Vector2d minMax = new Vector2d();
return CollisionMath.intersectRayAABB(
rayStart, rayDir, position.x, position.y, position.z, boundingBox, minMax);Surviving candidates are sorted by squared distance (distanceSquaredTo) and the nearest is returned. Note the geometry primitives in play: Box is the AABB (min/max corners, with width(), height(), depth(), and an isIntersecting(Box) overlap test), and it is one implementation of the Shape interface alongside Ellipsoid and Cylinder. Box also offers intersectsLine(start, end) — a segment-bounded slab test used directly for line-of-sight, which we will see next.
#Frustum hit detection: how melee actually swings
Block-ray and entity-ray tests answer "what is on this line." Melee is wider than a line — a stab has a near and far reach and a cross-sectional area. Hytale models this as a camera frustum and reuses graphics-style projection math through HitDetectionExecutor.
The executor is configured with a MatrixProvider for projection and another for view. StabSelector (a data-driven combat selector) drives an OrthogonalProjectionProvider whose near/far planes are animated over the swing's duration and whose left/right/top/bottom define the blade's reach box, plus a DirectionViewProvider aimed by head yaw/pitch:
// StabSelector.RuntimeSelector.tick — build the swing volume this frame
this.projectionProvider
.setNear(deltaStartDistance).setFar(deltaEndDistance) // reach grows over time
.setLeft(extendLeft).setRight(extendRight)
.setBottom(extendBottom).setTop(extendTop)
.setRotation(yawOffset, pitchOffset, rollOffset);
this.viewProvider
.setPosition(posX, posY, posZ)
.setDirection(headRotation.getYaw(), headRotation.getPitch());
this.executor.setOrigin(posX, posY, posZ)
.setProjectionProvider(this.projectionProvider)
.setViewProvider(this.viewProvider);To test an entity, the selector builds a model matrix from the target's hitbox and feeds the eight-corner HitDetectionExecutor.CUBE_QUADS (six Quad4d faces) into executor.test(model, modelMatrix):
// project the target's hitbox into the swing frustum, then test
this.modelMatrix.identity()
.translate(transformComponent.getPosition())
.translate(hitbox.getMin())
.scale(hitbox.width(), hitbox.height(), hitbox.depth());
if (this.executor.test(HitDetectionExecutor.CUBE_QUADS, this.modelMatrix)) {
consumer.accept(entity, this.executor.getHitLocation());
}Inside testModel, each quad is multiplied by the combined projection-view-model matrix and clipped against the frustum (insideFrustum runs Sutherland–Hodgman polygon clipping per axis via clipPolygonComponent). Surviving geometry yields a candidate Vector4d hit point; the executor keeps the closest one to the origin (minDistanceSquared) that also passes the LineOfSightProvider. A setMaxRayTests cap (default 10) bounds the per-call cost.
LineOfSightProvider is the seam where occlusion is enforced. Its default is LineOfSightProvider.DEFAULT_TRUE (everything is visible). When a StabSelector sets testLineOfSight, the provider runs a BlockIterator.iterateFromTo between attacker and target and, for each Solid block, tests every detail hitbox with Box.intersectsLine — returning false (occluded) on the first segment that clips a wall.
Orthographic, not perspective
StabSelector uses an OrthogonalProjectionProvider (projectionOrtho), so the swing volume is a box, not a cone — reach does not fan out with distance. FrustumProjectionProvider (projectionFrustum) exists for perspective-style selectors. Choosing the wrong projection silently changes how wide your weapon hits at range; the executor API is identical, only the MatrixProvider differs.
#Putting it together
A modder extending interaction or combat works with three composable tools. For "what am I looking at," march a BlockIterator and stop your procedure with return false on the first block your predicate accepts — or just call TargetUtil.getTargetBlock / getTargetLocation. For "did this ray touch that entity," ray-test its Box with CollisionMath.intersectRayAABB and keep the nearest by squared distance. For "what does this swing connect with," configure a HitDetectionExecutor with a projection and view MatrixProvider, optionally wire a LineOfSightProvider, and test the target's CUBE_QUADS against its model matrix.
Raycasting modder checklist
- Build your ray with
Vector3d.assign(yaw, pitch)and offset the origin bymodelComponent.getModel().getEyeHeight()— aim from the eyes, not the feet. - For block queries, prefer
TargetUtil.getTargetBlock/getTargetLocation; only drop to a rawBlockIteratorwhen you need custom per-voxel logic. - In a
BlockIteratorProcedure, returnfalseto stop, and remember the outer query inverts that boolean — completion means "no hit." - Cache chunk lookups across the walk (see
TargetUtil.TargetBuffer.updateChunk); re-resolve only on chunk-boundary crossings. - For entity rays, use
CollisionMath.intersectRayAABBfor an infinite ray orintersectVectorAABBwhen the displacement is finite (t ≤ 1). - Use
RaycastAABB'sRaycastConsumeroverload when you need the hit face normal, not just the distance. - For area weapons, pick
OrthogonalProjectionProvider(box reach) vs.FrustumProjectionProvider(perspective) deliberately, and setmaxRayTeststo bound cost. - Gate hits behind a
LineOfSightProviderwhen walls should block the swing; the defaultDEFAULT_TRUEignores occlusion entirely.
Build it yourself
Turn an idea into a working Hytale mod
Describe what you want in plain English and get a real, installable build. No boilerplate, no setup.
Start building