extends SceneTree
## WHICH WINDING DOES A CONCAVE COLLISION SHAPE COLLIDE ON?
##
## godot --headless --path . --script res://scenes/test/trimesh_winding_gate.gd
##
## dc-terrain hands PhysicsServer3D a raw triangle soup
## (shape_set_data({"faces": ...})) built straight from the mesher's index
## buffer. Nothing has ever checked that those triangles face the way Godot's
## concave shape expects, and the question is not answerable by reading either
## codebase: Godot's Plane(a,b,c) uses (a-c)x(a-b) while the textbook normal is
## (b-a)x(c-a), and the two are negatives. I picked one of those conventions
## earlier in the session because the other "looked wrong" -- which is choosing
## an answer, not measuring one.
##
## So: build a floor of known winding, drop the SHIPPING capsule on it, and read
## the normal the physics server reports. Then build the reversed floor and do
## it again. The winding that yields a normal of +Y is the one a floor must
## have, and it is measured rather than argued.
##
## Exit 0 if the two windings are distinguishable and CCW-when-seen-from-above
## is the floor. Non-zero if the test itself could not tell them apart, which
## would mean this gate proves nothing.
const R := 0.3
const H := 1.58
func _initialize() -> void:
var ccw := _drop(_floor_tris(true))
var cw := _drop(_floor_tris(false))
print("CCW seen from above (normal (b-a)x(c-a) = +Y): %s" % _fmt(ccw))
print("CW seen from above (normal (b-a)x(c-a) = -Y): %s" % _fmt(cw))
var ok := true
# 1. The two must differ. If both behave identically the shape is
# double-sided and winding is not the explanation for anything.
if ccw["hit"] == cw["hit"] and ccw["normal"].is_equal_approx(cw["normal"]):
print("BOTH WINDINGS BEHAVE IDENTICALLY — the shape is double-sided;")
print("winding cannot explain an inverted contact normal.")
ok = false
# 2. Name which one is a floor.
for label in ["ccw", "cw"]:
var r: Dictionary = ccw if label == "ccw" else cw
if r["hit"] and r["normal"].dot(Vector3.UP) > 0.9:
print("=> %s winding gives a floor normal of +Y. THAT is the winding a" % label.to_upper())
print(" terrain collision triangle must have.")
for label in ["ccw", "cw"]:
var r2: Dictionary = ccw if label == "ccw" else cw
if r2["hit"] and r2["normal"].dot(Vector3.UP) < -0.9:
print("=> %s winding collides with an INVERTED normal (pointing down)." % label.to_upper())
print(" A terrain wound this way is exactly the 163-168 degree contact")
print(" measured on 15 degree ground.")
quit(0 if ok else 1)
## Two triangles making a 20x20 m floor at y = 0.
## ccw_from_above true -> (b-a)x(c-a) points +Y
## ccw_from_above false -> the same quad with both triangles reversed
func _floor_tris(ccw_from_above: bool) -> PackedVector3Array:
var a := Vector3(-10, 0, -10)
var b := Vector3(10, 0, -10)
var c := Vector3(10, 0, 10)
var d := Vector3(-10, 0, 10)
var f := PackedVector3Array()
if ccw_from_above:
f.append_array([a, c, b])
f.append_array([a, d, c])
else:
f.append_array([a, b, c])
f.append_array([a, c, d])
# State the convention this function believes, computed rather than assumed.
var n := (f[1] - f[0]).cross(f[2] - f[0]).normalized()
print(" built floor: (b-a)x(c-a) = %s" % str(n))
return f
func _drop(faces: PackedVector3Array) -> Dictionary:
var world := World3D.new()
var shape: RID = PhysicsServer3D.concave_polygon_shape_create()
PhysicsServer3D.shape_set_data(shape, {"faces": faces})
var body: RID = PhysicsServer3D.body_create()
PhysicsServer3D.body_set_mode(body, PhysicsServer3D.BODY_MODE_STATIC)
PhysicsServer3D.body_add_shape(body, shape)
PhysicsServer3D.body_set_space(body, world.space)
PhysicsServer3D.body_set_state(body, PhysicsServer3D.BODY_STATE_TRANSFORM, Transform3D())
# The SHIPPING capsule, so the answer is about this character.
var cap := CapsuleShape3D.new()
cap.radius = R
cap.height = H
var params := PhysicsTestMotionParameters3D.new()
params.from = Transform3D(Basis(), Vector3(0, 1.5, 0))
params.motion = Vector3(0, -3.0, 0)
var char_body: RID = PhysicsServer3D.body_create()
PhysicsServer3D.body_set_mode(char_body, PhysicsServer3D.BODY_MODE_KINEMATIC)
PhysicsServer3D.body_add_shape(char_body, cap.get_rid())
PhysicsServer3D.body_set_space(char_body, world.space)
PhysicsServer3D.body_set_state(char_body, PhysicsServer3D.BODY_STATE_TRANSFORM, params.from)
var res := PhysicsTestMotionResult3D.new()
var hit: bool = PhysicsServer3D.body_test_motion(char_body, params, res)
var out := {
"hit": hit,
"normal": res.get_collision_normal() if hit else Vector3.ZERO,
"depth": res.get_collision_depth() if hit else 0.0,
}
PhysicsServer3D.free_rid(char_body)
PhysicsServer3D.free_rid(body)
PhysicsServer3D.free_rid(shape)
return out
func _fmt(r: Dictionary) -> String:
if not r["hit"]:
return "NO COLLISION AT ALL"
return "hit, normal %s (%.1f deg from up)" % [str(r["normal"]),
rad_to_deg(acos(clampf((r["normal"] as Vector3).dot(Vector3.UP), -1.0, 1.0)))]