Skip to content

Hull trace gives wrong results when casting against triangle mesh #11354

Description

@Phyksar

Version / Commit

26.06.24 (6/24/2026 7:26:44 PM)

Describe the bug

You can see the exact problem in the video. The cylinder shape intersection becomes way too off near the edge of the triangle and the hit normal (blue vector) is completely wrong, presumably it tries to compute an intersection with the vertical triangle which should not happen because the cylinder has a slight 1 degree rotation away from it. The number in the video is just the triangle index.

2026-06-30.04-27-58.mp4

Another problem is presumably related, when moving shape through the so called "quad diagonal", the normal suddenly changes the way it shouldn't.

2026-06-30.04-10-28.mp4

To Reproduce

  1. Create a new component SceneTraceTest with a code down below.
  2. Add new empty game object to the scene and add SceneTraceTest component.
  3. The collider and the tracing shape will be spawned with the right parameters to reproduce the issue.
  4. You can move the Origin child of SceneTraceTest to watch the inconsistent behaviour of hull tracing.
SceneTraceTest.cs
using System;

public sealed class SceneTraceTest : Component, Component.ExecuteInEditor
{
	private const float CubeSize = 16.0f;
	private const int CylinderSegments = 16;
	private const float RelativeArrowLength = 0.3f;
	private const float RelativeArrowWidth = 0.1f;
	private static readonly Vector3 LocalColliderDirection = Vector3.Down;
	private static readonly Rotation LocalColliderRotation = Rotation.FromRoll( 90.0f );

	private static readonly Vector3 LocalOriginPosition = new Vector3( -9.04504585f, 19.3929996f, 54.4659996f );
	private static readonly Rotation LocalOriginRotation = new Rotation( 0.00822092313f, -0.0789783895f, 0.000651332026f, 0.996842206f );

	private ModelCollider Collider;
	private SceneTraceResult[] _traceResults;

	[Property]
	public GameObject Origin { get; set; }

	[Property]
	public float Radius { get; set; } = 16.0f;

	[Property]
	public float Height { get; set; } = 8.0f;

	[Property]
	public float Distance { get; set; } = 64.0f;

	[Property]
	public string CollisionTag { get; set; } = "solid";

	[Property, ReadOnly]
	public int NumResults => _traceResults?.Length ?? 0;

	protected override void OnFixedUpdate()
	{
		if ( !Collider.IsValid() )
			Collider = GetComponent<ModelCollider>( includeDisabled: true );

		if ( !Collider.IsValid() )
			Collider = AddComponent<ModelCollider>();

		if ( !Collider.Model.IsValid() || Collider.Model.IsError )
			Collider.Model = CreateCollisionModel();

		if ( !Origin.IsValid() )
			Origin = GameObject.Children.FirstOrDefault();

		if ( !Origin.IsValid() )
		{
			Origin = new GameObject( GameObject, true, "Origin" );
			Origin.LocalPosition = LocalOriginPosition;
			Origin.LocalRotation = LocalOriginRotation;
			Origin.LocalScale = Vector3.One;
		}
	}

	protected override void DrawGizmos()
	{
		if ( !Origin.IsValid() || !Collider.IsValid() )
			return;

		_traceResults = Scene.Trace.Cylinder( Height, Radius )
			.Rotated( Origin.WorldRotation * LocalColliderRotation )
			.FromTo( Origin.WorldPosition, Origin.WorldPosition + Origin.WorldRotation * LocalColliderDirection * Distance )
			.WithCollisionRules( CollisionTag )
			.UseHitPosition( true )
			.RunAll()
			.ToArray();
		Gizmo.Transform = Origin.WorldTransform.WithScale( 1.0f );
		Gizmo.Draw.Color = Color.White;
		Gizmo.Draw.LineThickness = 2.0f;
		Gizmo.Draw.Line( Vector3.Zero, LocalColliderDirection * Distance );

		Gizmo.Draw.Color = Gizmo.Colors.Green;
		Gizmo.Draw.LineThickness = 1.0f;
		if ( !_traceResults.Any() )
        {
			DrawLineCylinder( LocalColliderDirection * Distance, LocalColliderRotation, Radius, Height );
        }
		foreach ( var result in _traceResults )
		{
			var localPosition = LocalColliderDirection * (result.Hit ? result.Distance : Distance);
			DrawLineCylinder( localPosition, LocalColliderRotation, Radius, Height );
			DebugOverlay.Text( result.HitPosition, $"{result.Triangle}", 16.0f );

			if ( result.Hit )
			{
				var left = Origin.WorldRotation.Left;
				var forward = Vector3.Cross( left, result.Normal ).Normal;
				left = Vector3.Cross( result.Normal, forward ).Normal;
				DrawAxes( result.HitPosition, forward, left, result.Normal, 8.0f );
			}
		}
	}

	private Model CreateCollisionModel()
	{
		var vertices = new List<Vector3>
		{
			new Vector3( CubeSize, CubeSize, CubeSize ),
			new Vector3( -CubeSize, CubeSize, CubeSize ),
			new Vector3( CubeSize, -CubeSize, CubeSize ),
			new Vector3( -CubeSize, -CubeSize, CubeSize ),
			new Vector3( CubeSize, CubeSize, -CubeSize ),
			new Vector3( -CubeSize, CubeSize, -CubeSize ),
			new Vector3( CubeSize, -CubeSize, -CubeSize ),
			new Vector3( -CubeSize, -CubeSize, -CubeSize )
		};
		var indices = new List<int>
		{
			0, 2, 1,  1, 2, 3,
			0, 1, 4,  1, 5, 4,
			1, 3, 5,  3, 7, 5,
			0, 6, 2,  0, 4, 6,
			2, 6, 3,  3, 6, 7
		};
		var modelBuilder = new ModelBuilder();
		modelBuilder.AddCollisionMesh( vertices, indices );
		return modelBuilder.Create();
	}

	private void DrawLineCylinder( in Vector3 position, in Rotation rotation, float radius, float height )
	{
		const int BrigdeGap = CylinderSegments / 4;
		var halfHeight = 0.5f * height;
		float angle, nextX, nextZ;
		var lastX = radius;
		var lastY = 0.0f;
		for ( var i = 0; i < CylinderSegments; i++ )
		{
			angle = 2.0f * (i + 1) * MathF.PI / CylinderSegments;
			nextX = radius * MathF.Cos( angle );
			nextZ = radius * MathF.Sin( angle );
			Gizmo.Draw.Line(
				position + rotation * new Vector3( lastX, lastY, halfHeight ),
				position + rotation * new Vector3( nextX, nextZ, halfHeight ) );
			if ( height != 0.0f )
			{
				Gizmo.Draw.Line(
					position + rotation * new Vector3( lastX, lastY, -halfHeight ),
					position + rotation * new Vector3( nextX, nextZ, -halfHeight ) );
				if ( i % BrigdeGap == 0 )
				{
					Gizmo.Draw.Line(
						position + rotation * new Vector3( lastX, lastY, halfHeight ),
						position + rotation * new Vector3( lastX, lastY, -halfHeight ) );
				}
			}
			lastX = nextX;
			lastY = nextZ;
		}
	}

	private void DrawAxes( in Vector3 position, in Vector3 forward, Vector3 left, in Vector3 up, float size )
	{
		using ( Gizmo.Scope() )
		{
			Gizmo.Transform = global::Transform.Zero;
			Gizmo.Draw.LineThickness = 3.0f;
			Gizmo.Draw.Color = Gizmo.Colors.Forward;
			Gizmo.Draw.Arrow( position, position + forward * size, RelativeArrowLength * size, RelativeArrowWidth * size );
			Gizmo.Draw.Color = Gizmo.Colors.Left;
			Gizmo.Draw.Arrow( position, position + left * size, RelativeArrowLength * size, RelativeArrowWidth * size );
			Gizmo.Draw.Color = Gizmo.Colors.Up;
			Gizmo.Draw.Arrow( position, position + up * size, RelativeArrowLength * size, RelativeArrowWidth * size );
		}
	}
}

Expected behavior

The hull trace should give consistent correct results when cast against triangle meshes.

Media/Files

No response

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions