Module: Quake::Bsp::Vis
- Defined in:
- lib/quake/bsp/vis.rb
Overview
PVS (Potentially Visible Set) decompression and leaf lookup. Each leaf stores a compressed bitvector indicating which other leaves are visible from it. The compression is simple RLE: a zero byte is followed by a count of zero bytes to insert.
Class Method Summary collapse
- .add_to_fat_pvs(level, point, node_index, num_leafs, result) ⇒ Object
- .all_visible(num_leafs) ⇒ Object
-
.decompress_pvs(visibility, vis_offset, num_leafs) ⇒ Object
Decompress the PVS for a leaf into an array of visible leaf indices.
-
.fat_pvs(level, point) ⇒ Object
Compute a “fat” PVS by unioning the PVS of every leaf within ~8 units of the given point.
-
.liquid_face_to_leaves(level) ⇒ Object
Precompute, for each liquid (turb) face, the two leaves it physically separates (computed via BSP traversal of a point on either side of the face’s plane).
-
.near_liquid_portal?(level, leaf) ⇒ Boolean
True if any of the given leaf’s marksurfaces is a turbulent (liquid) surface.
-
.point_in_leaf(level, point) ⇒ Object
Find which leaf a point is in by walking the BSP node tree.
-
.visible_faces(level, leaf_index, point: nil) ⇒ Object
Mark which faces are visible from the given leaf.
Class Method Details
.add_to_fat_pvs(level, point, node_index, num_leafs, result) ⇒ Object
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/quake/bsp/vis.rb', line 87 def self.add_to_fat_pvs(level, point, node_index, num_leafs, result) loop do if node_index < 0 leaf_index = ~node_index return if leaf_index == 0 # skip the universal solid leaf leaf = level.leafs[leaf_index] return if leaf.nil? result.merge(decompress_pvs(level.visibility, leaf.vis_offset, num_leafs)) return end node = level.nodes[node_index] plane = level.planes[node.plane_index] d = point.dot(plane.normal) - plane.dist if d > 8.0 node_index = node.children[0] elsif d < -8.0 node_index = node.children[1] else # Straddling the plane within the fat radius - descend both sides add_to_fat_pvs(level, point, node.children[0], num_leafs, result) node_index = node.children[1] end end end |
.all_visible(num_leafs) ⇒ Object
241 242 243 |
# File 'lib/quake/bsp/vis.rb', line 241 def self.all_visible(num_leafs) Set.new(1..num_leafs) end |
.decompress_pvs(visibility, vis_offset, num_leafs) ⇒ Object
Decompress the PVS for a leaf into an array of visible leaf indices. visibility - raw visibility lump data (String) vis_offset - byte offset into visibility data for this leaf num_leafs - total number of leafs in the level (excluding leaf 0) Returns a Set of visible leaf indices (1-based, matching leafs array).
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/quake/bsp/vis.rb', line 15 def self.decompress_pvs(visibility, vis_offset, num_leafs) return all_visible(num_leafs) if vis_offset < 0 || visibility.nil? || visibility.empty? num_bytes = (num_leafs + 7) / 8 pvs = String.new("\0" * num_bytes, encoding: Encoding::BINARY) src = vis_offset dst = 0 while dst < num_bytes byte = visibility.getbyte(src) # Vis data ended before filling the bitvector. Remaining bytes # are already zero (no additional visible leaves), which is safe. break if byte.nil? src += 1 if byte != 0 pvs.setbyte(dst, byte) dst += 1 else # RLE: next byte is count of zero bytes count = visibility.getbyte(src) || 0 src += 1 dst += count # pvs already zeroed end end # Convert bitvector to set of leaf indices visible = Set.new num_leafs.times do |i| byte_idx = i / 8 bit_idx = i % 8 if pvs.getbyte(byte_idx) & (1 << bit_idx) != 0 visible << (i + 1) # leaf indices are 1-based (leaf 0 is the solid leaf) end end visible end |
.fat_pvs(level, point) ⇒ Object
Compute a “fat” PVS by unioning the PVS of every leaf within ~8 units of the given point. Used near translucent water surfaces so the underwater leaves (which standard PVS excludes from above-water leaves) are included; without this the cave bed isn’t drawn and alpha-blended water shows the clear color through gaps. Port of quakespasm SV_FatPVS / SV_AddToFatPVS.
80 81 82 83 84 85 |
# File 'lib/quake/bsp/vis.rb', line 80 def self.fat_pvs(level, point) num_leafs = level.leafs.size - 1 result = Set.new add_to_fat_pvs(level, point, 0, num_leafs, result) result end |
.liquid_face_to_leaves(level) ⇒ Object
Precompute, for each liquid (turb) face, the two leaves it physically separates (computed via BSP traversal of a point on either side of the face’s plane). The standard Quake VIS compiler treats water as opaque, so above-water and underwater leaves never see each other in the PVS. Marksurface lists are also unreliable here - many water faces are marked in only one leaf - so we use geometry instead. We use this index at runtime to “vis-through” water surfaces: when a water face is in the visible set, also include the leaves on its other side. Without this, translucent water alpha-blends against the clear color where the cave bed should be.
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 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/quake/bsp/vis.rb', line 140 def self.liquid_face_to_leaves(level) @liquid_face_to_leaves_cache ||= {} cached = @liquid_face_to_leaves_cache[level.object_id] return cached if cached index = {} level.faces.each_with_index do |face, face_idx| next if face.nil? ti = level.texinfo[face.texinfo_index] next if ti.nil? tex = level.textures[ti.miptex_index] next unless tex && tex.name.start_with?("*") # Compute face center cx = cy = cz = 0.0 n = face.num_edges n.times do |i| se = level.surfedges[face.first_edge + i] edge = level.edges[se.abs] v = se >= 0 ? level.vertices[edge.v0] : level.vertices[edge.v1] cx += v.x; cy += v.y; cz += v.z end cx /= n; cy /= n; cz /= n plane = level.planes[face.plane_index] # Step a small distance along + and - the plane normal, find the # leaf each point lands in. offsets = [1.0, -1.0] leaves = offsets.map do |o| pt = Math::Vec3.new( cx + plane.normal.x * o, cy + plane.normal.y * o, cz + plane.normal.z * o ) point_in_leaf(level, pt) end.uniq.reject { |l| l == 0 } # skip solid leaf index[face_idx] = leaves end @liquid_face_to_leaves_cache[level.object_id] = index index end |
.near_liquid_portal?(level, leaf) ⇒ Boolean
True if any of the given leaf’s marksurfaces is a turbulent (liquid) surface. Quakespasm’s “near water portal” check.
116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/quake/bsp/vis.rb', line 116 def self.near_liquid_portal?(level, leaf) return false if leaf.nil? leaf.num_marksurfaces.times do |i| face_idx = level.marksurfaces[leaf.first_marksurface + i] face = level.faces[face_idx] next if face.nil? ti = level.texinfo[face.texinfo_index] next if ti.nil? tex = level.textures[ti.miptex_index] return true if tex && tex.name.start_with?("*") end false end |
.point_in_leaf(level, point) ⇒ Object
Find which leaf a point is in by walking the BSP node tree.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/quake/bsp/vis.rb', line 55 def self.point_in_leaf(level, point) node_index = 0 loop do node = level.nodes[node_index] plane = level.planes[node.plane_index] dist = point.dot(plane.normal) - plane.dist child = dist >= 0 ? node.children[0] : node.children[1] # Negative child index means leaf: ~child gives leaf index if child < 0 return ~child else node_index = child end end end |
.visible_faces(level, leaf_index, point: nil) ⇒ Object
Mark which faces are visible from the given leaf. Returns a Set of face indices that should be rendered.
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 |
# File 'lib/quake/bsp/vis.rb', line 186 def self.visible_faces(level, leaf_index, point: nil) leaf = level.leafs[leaf_index] return Set.new if leaf.nil? num_leafs = level.leafs.size - 1 # exclude leaf 0 # Leaf 0 is the universal SOLID leaf and has no real PVS; its # vis_offset is unused but Quake BSPs sometimes store 0 here, which # would otherwise decompress garbage. Match TyrQuake Mod_LeafPVS: # treat the solid leaf as all-visible so a camera that ends up in # solid space (noclip, edge cases) still sees the world. visible_leafs = if leaf_index == 0 all_visible(num_leafs) elsif point && near_liquid_portal?(level, leaf) # Camera leaf touches a liquid surface. Use FatPVS # so the underwater leaves are included and # translucent water doesn't reveal void where the # bed should be. fat_pvs(level, point) else decompress_pvs(level.visibility, leaf.vis_offset, num_leafs) end face_set = Set.new leafs_to_walk = visible_leafs.dup leafs_to_walk << leaf_index # also include current leaf # Vis-through water: any visible water face brings the leaves on its # other side into view too. Standard Quake PVS treats water as # opaque, so without this expansion translucent water from above # blends against void instead of the underwater cave bed. liquid_index = liquid_face_to_leaves(level) seen_leafs = Set.new worklist = leafs_to_walk.to_a while (li = worklist.shift) next unless seen_leafs.add?(li) vl = level.leafs[li] next if vl.nil? vl.num_marksurfaces.times do |i| face_idx = level.marksurfaces[vl.first_marksurface + i] face_set << face_idx # If this is a water face, queue the leaves on its other side. other_leaves = liquid_index[face_idx] next unless other_leaves other_leaves.each do |other_leaf| worklist << other_leaf unless seen_leafs.include?(other_leaf) end end end face_set end |