Module: Quake::Physics::HullTrace

Defined in:
lib/quake/physics/hull_trace.rb

Class Method Summary collapse

Class Method Details

.hull0_clipnodes(level) ⇒ Object

Quake's Mod_MakeHull0: duplicate the render node tree as clipnodes so point traces (hull 0) can reuse the same recursive hull check. A node child < 0 encodes leaf -(child + 1); hull 0 replaces it with the leaf contents, exactly like the C loader.



102
103
104
105
106
107
108
109
110
111
112
# File 'lib/quake/physics/hull_trace.rb', line 102

def self.hull0_clipnodes(level)
  return @hull0_clipnodes if @hull0_level&.equal?(level)

  @hull0_level = level
  @hull0_clipnodes = level.nodes.map do |node|
    children = node.children.map do |child|
      child >= 0 ? child : level.leafs[-child - 1].contents
    end
    Quake::Bsp::ClipNode.new(plane_index: node.plane_index, children: children)
  end
end

.hull_for_box(mins, maxs) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/quake/physics/hull_trace.rb', line 28

def self.hull_for_box(mins, maxs)
  clipnodes = 6.times.map do |i|
    side = i & 1
    children = Array.new(2)
    children[side] = CONTENTS_EMPTY
    children[side ^ 1] = i == 5 ? CONTENTS_SOLID : i + 1

    Quake::Bsp::ClipNode.new(plane_index: i, children: children)
  end

  planes = [
    box_plane(type: 0, dist: maxs.x),
    box_plane(type: 0, dist: mins.x),
    box_plane(type: 1, dist: maxs.y),
    box_plane(type: 1, dist: mins.y),
    box_plane(type: 2, dist: maxs.z),
    box_plane(type: 2, dist: mins.z)
  ]

  BoxHull.new(
    clipnodes: clipnodes, planes: planes,
    first_clipnode: 0, last_clipnode: 5
  )
end

.point_contents(clipnodes, planes, node_index, point) ⇒ Object

Check what content type a point is in, using the clipnode tree. hull_clipnodes: array of ClipNode planes: array of Plane node_index: starting clipnode index point: Vec3



58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/quake/physics/hull_trace.rb', line 58

def self.point_contents(clipnodes, planes, node_index, point)
  while node_index >= 0
    if node_index >= clipnodes.size
      raise "SV_HullPointContents: bad node number"
    end

    node = clipnodes[node_index]
    plane = planes[node.plane_index]

    dist = plane_distance(plane, point)
    node_index = dist >= 0 ? node.children[0] : node.children[1]
  end
  node_index # negative value = content type
end

.trace(clipnodes, planes, node_index, p1, p2) ⇒ Object

Trace a line from p1 to p2 through the hull. Returns a TraceResult.



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/quake/physics/hull_trace.rb', line 75

def self.trace(clipnodes, planes, node_index, p1, p2)
  result = {
    all_solid: false, start_solid: false,
    in_open: false, in_water: false,
    fraction: 1.0, end_pos: p2,
    plane_normal: nil, plane_dist: nil
  }

  recursive_trace(clipnodes, planes, node_index, 0.0, 1.0, p1, p2, result)

  if result[:fraction] == 1.0
    result[:end_pos] = p2
  else
    result[:end_pos] = Math::Vec3.new(
      p1.x + result[:fraction] * (p2.x - p1.x),
      p1.y + result[:fraction] * (p2.y - p1.y),
      p1.z + result[:fraction] * (p2.z - p1.z)
    )
  end

  TraceResult.new(**result)
end

.trace_world_and_entities(level, p1, p2, brush_entities, hull_num: 1) ⇒ Object

Trace against the world AND all solid brush entities, returning the nearest hit. Brush entity traces are done in entity-local space (subtract origin, trace sub-model hull, transform back). hull_num 0 is a point trace through the render-node hull, matching Quake's SV_TraceLine with MOVE_NOMONSTERS (world + SOLID_BSP only).



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/quake/physics/hull_trace.rb', line 119

def self.trace_world_and_entities(level, p1, p2, brush_entities, hull_num: 1)
  clipnodes = hull_num.zero? ? hull0_clipnodes(level) : level.clipnodes
  planes = level.planes

  # World trace
  world_hull = level.models[0].head_nodes[hull_num]
  world_hull = level.models[0].head_nodes[0] if world_hull < 0 || world_hull >= clipnodes.size
  best = trace(clipnodes, planes, world_hull, p1, p2)

  # Trace against each brush entity's sub-model
  brush_entities&.each do |ent|
    next unless ent.brush_entity?
    next if ent.removed?
    # Triggers are SOLID_TRIGGER in Quake: they detect overlap but
    # don't block movement. They're handled by check_triggers.
    next if ent.classname.start_with?("trigger_")
    next if ent.classname == "func_illusionary"
    model = level.models[ent.model_index]
    next unless model

    sub_hull = model.head_nodes[hull_num]
    # Many sub-models only have hull 0; fall back gracefully
    sub_hull = model.head_nodes[0] if sub_hull < 0 || sub_hull >= clipnodes.size
    next if sub_hull < 0 || sub_hull >= clipnodes.size

    # Transform trace into entity-local space
    offset = ent.position
    local_p1 = Math::Vec3.new(p1.x - offset.x, p1.y - offset.y, p1.z - offset.z)
    local_p2 = Math::Vec3.new(p2.x - offset.x, p2.y - offset.y, p2.z - offset.z)

    sub_result = trace(clipnodes, planes, sub_hull, local_p1, local_p2)

    # Keep nearest hit
    if sub_result.fraction < best.fraction
      # Transform end_pos back to world space
      ep = sub_result.end_pos
      best = TraceResult.new(
        all_solid: sub_result.all_solid,
        start_solid: sub_result.start_solid,
        in_open: sub_result.in_open,
        in_water: sub_result.in_water,
        fraction: sub_result.fraction,
        end_pos: Math::Vec3.new(ep.x + offset.x, ep.y + offset.y, ep.z + offset.z),
        plane_normal: sub_result.plane_normal,
        plane_dist: sub_result.plane_dist
      )
    end
  end

  best
end