quake-rb 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/quake/bsp/vis.rb CHANGED
@@ -71,119 +71,9 @@ module Quake
71
71
  end
72
72
  end
73
73
 
74
- # Compute a "fat" PVS by unioning the PVS of every leaf within ~8 units
75
- # of the given point. Used near translucent water surfaces so the
76
- # underwater leaves (which standard PVS excludes from above-water
77
- # leaves) are included; without this the cave bed isn't drawn and
78
- # alpha-blended water shows the clear color through gaps.
79
- # Port of quakespasm SV_FatPVS / SV_AddToFatPVS.
80
- def self.fat_pvs(level, point)
81
- num_leafs = level.leafs.size - 1
82
- result = Set.new
83
- add_to_fat_pvs(level, point, 0, num_leafs, result)
84
- result
85
- end
86
-
87
- def self.add_to_fat_pvs(level, point, node_index, num_leafs, result)
88
- loop do
89
- if node_index < 0
90
- leaf_index = ~node_index
91
- return if leaf_index == 0 # skip the universal solid leaf
92
- leaf = level.leafs[leaf_index]
93
- return if leaf.nil?
94
- result.merge(decompress_pvs(level.visibility, leaf.vis_offset, num_leafs))
95
- return
96
- end
97
-
98
- node = level.nodes[node_index]
99
- plane = level.planes[node.plane_index]
100
- d = point.dot(plane.normal) - plane.dist
101
-
102
- if d > 8.0
103
- node_index = node.children[0]
104
- elsif d < -8.0
105
- node_index = node.children[1]
106
- else
107
- # Straddling the plane within the fat radius - descend both sides
108
- add_to_fat_pvs(level, point, node.children[0], num_leafs, result)
109
- node_index = node.children[1]
110
- end
111
- end
112
- end
113
-
114
- # True if any of the given leaf's marksurfaces is a turbulent (liquid)
115
- # surface. Quakespasm's "near water portal" check.
116
- def self.near_liquid_portal?(level, leaf)
117
- return false if leaf.nil?
118
- leaf.num_marksurfaces.times do |i|
119
- face_idx = level.marksurfaces[leaf.first_marksurface + i]
120
- face = level.faces[face_idx]
121
- next if face.nil?
122
- ti = level.texinfo[face.texinfo_index]
123
- next if ti.nil?
124
- tex = level.textures[ti.miptex_index]
125
- return true if tex && tex.name.start_with?("*")
126
- end
127
- false
128
- end
129
-
130
- # Precompute, for each liquid (turb) face, the two leaves it physically
131
- # separates (computed via BSP traversal of a point on either side of
132
- # the face's plane). The standard Quake VIS compiler treats water as
133
- # opaque, so above-water and underwater leaves never see each other in
134
- # the PVS. Marksurface lists are also unreliable here - many water
135
- # faces are marked in only one leaf - so we use geometry instead. We
136
- # use this index at runtime to "vis-through" water surfaces: when a
137
- # water face is in the visible set, also include the leaves on its
138
- # other side. Without this, translucent water alpha-blends against
139
- # the clear color where the cave bed should be.
140
- def self.liquid_face_to_leaves(level)
141
- @liquid_face_to_leaves_cache ||= {}
142
- cached = @liquid_face_to_leaves_cache[level.object_id]
143
- return cached if cached
144
-
145
- index = {}
146
- level.faces.each_with_index do |face, face_idx|
147
- next if face.nil?
148
- ti = level.texinfo[face.texinfo_index]
149
- next if ti.nil?
150
- tex = level.textures[ti.miptex_index]
151
- next unless tex && tex.name.start_with?("*")
152
-
153
- # Compute face center
154
- cx = cy = cz = 0.0
155
- n = face.num_edges
156
- n.times do |i|
157
- se = level.surfedges[face.first_edge + i]
158
- edge = level.edges[se.abs]
159
- v = se >= 0 ? level.vertices[edge.v0] : level.vertices[edge.v1]
160
- cx += v.x; cy += v.y; cz += v.z
161
- end
162
- cx /= n; cy /= n; cz /= n
163
-
164
- plane = level.planes[face.plane_index]
165
- # Step a small distance along + and - the plane normal, find the
166
- # leaf each point lands in.
167
- offsets = [1.0, -1.0]
168
- leaves = offsets.map do |o|
169
- pt = Math::Vec3.new(
170
- cx + plane.normal.x * o,
171
- cy + plane.normal.y * o,
172
- cz + plane.normal.z * o
173
- )
174
- point_in_leaf(level, pt)
175
- end.uniq.reject { |l| l == 0 } # skip solid leaf
176
-
177
- index[face_idx] = leaves
178
- end
179
-
180
- @liquid_face_to_leaves_cache[level.object_id] = index
181
- index
182
- end
183
-
184
74
  # Mark which faces are visible from the given leaf.
185
75
  # Returns a Set of face indices that should be rendered.
186
- def self.visible_faces(level, leaf_index, point: nil)
76
+ def self.visible_faces(level, leaf_index)
187
77
  leaf = level.leafs[leaf_index]
188
78
  return Set.new if leaf.nil?
189
79
 
@@ -195,43 +85,18 @@ module Quake
195
85
  # solid space (noclip, edge cases) still sees the world.
196
86
  visible_leafs = if leaf_index == 0
197
87
  all_visible(num_leafs)
198
- elsif point && near_liquid_portal?(level, leaf)
199
- # Camera leaf touches a liquid surface. Use FatPVS
200
- # so the underwater leaves are included and
201
- # translucent water doesn't reveal void where the
202
- # bed should be.
203
- fat_pvs(level, point)
204
88
  else
205
89
  decompress_pvs(level.visibility, leaf.vis_offset, num_leafs)
206
90
  end
207
91
 
208
92
  face_set = Set.new
209
- leafs_to_walk = visible_leafs.dup
210
- leafs_to_walk << leaf_index # also include current leaf
211
-
212
- # Vis-through water: any visible water face brings the leaves on its
213
- # other side into view too. Standard Quake PVS treats water as
214
- # opaque, so without this expansion translucent water from above
215
- # blends against void instead of the underwater cave bed.
216
- liquid_index = liquid_face_to_leaves(level)
217
- seen_leafs = Set.new
218
- worklist = leafs_to_walk.to_a
219
-
220
- while (li = worklist.shift)
221
- next unless seen_leafs.add?(li)
93
+ visible_leafs.each do |li|
222
94
  vl = level.leafs[li]
223
95
  next if vl.nil?
224
96
 
225
97
  vl.num_marksurfaces.times do |i|
226
98
  face_idx = level.marksurfaces[vl.first_marksurface + i]
227
99
  face_set << face_idx
228
-
229
- # If this is a water face, queue the leaves on its other side.
230
- other_leaves = liquid_index[face_idx]
231
- next unless other_leaves
232
- other_leaves.each do |other_leaf|
233
- worklist << other_leaf unless seen_leafs.include?(other_leaf)
234
- end
235
100
  end
236
101
  end
237
102
 
data/lib/quake/camera.rb CHANGED
@@ -5,7 +5,7 @@ require "glu"
5
5
 
6
6
  module Quake
7
7
  class Camera
8
- attr_accessor :position, :yaw, :pitch
8
+ attr_accessor :position, :yaw, :pitch, :roll
9
9
  attr_reader :fov, :near, :far
10
10
 
11
11
  SPEED = 320.0 # units per second (Quake run speed)
@@ -13,10 +13,11 @@ module Quake
13
13
  MAX_MOUSE_DELTA = 50 # clamp insane deltas (e.g. first frame warp)
14
14
 
15
15
  def initialize(position: Math::Vec3::ORIGIN, yaw: 0.0, pitch: 0.0,
16
- fov: 90.0, near: 4.0, far: 4096.0)
16
+ fov: 90.0, near: 4.0, far: 6144.0)
17
17
  @position = position
18
18
  @yaw = yaw
19
19
  @pitch = pitch
20
+ @roll = 0.0
20
21
  @fov = fov
21
22
  @near = near
22
23
  @far = far
@@ -24,25 +25,15 @@ module Quake
24
25
  end
25
26
 
26
27
  def forward
27
- # Quake: X = forward, Y = left, Z = up
28
- # yaw rotates around Z, pitch rotates around Y
29
- ry = deg2rad(@yaw)
30
- rp = deg2rad(@pitch)
31
- cp = ::Math.cos(rp)
32
- Math::Vec3.new(
33
- ::Math.cos(ry) * cp,
34
- ::Math.sin(ry) * cp,
35
- -::Math.sin(rp)
36
- )
28
+ quake_angle_vectors.fetch(:forward)
37
29
  end
38
30
 
39
31
  def right
40
- ry = deg2rad(@yaw - 90.0)
41
- Math::Vec3.new(::Math.cos(ry), ::Math.sin(ry), 0.0)
32
+ quake_angle_vectors.fetch(:right)
42
33
  end
43
34
 
44
35
  def up
45
- Math::Vec3.new(0.0, 0.0, 1.0)
36
+ quake_angle_vectors.fetch(:up)
46
37
  end
47
38
 
48
39
  def move_forward(dt)
@@ -72,7 +63,39 @@ module Quake
72
63
  def apply_projection_gl(aspect)
73
64
  GL.MatrixMode(GL::PROJECTION)
74
65
  GL.LoadIdentity
75
- GLU.Perspective(@fov, aspect, @near, @far)
66
+ GLU.Perspective(fov_y(aspect), aspect, @near, @far)
67
+ end
68
+
69
+ # Quake's fov cvar is the horizontal FOV on a 4:3 screen. Like
70
+ # Quakespasm's fov_adapt (Hor+), the vertical FOV stays at its 4:3
71
+ # value on wide screens and the horizontal FOV widens instead --
72
+ # otherwise widescreen crops the view and the weapon model.
73
+ def fov_y(_aspect = nil)
74
+ 2.0 * ::Math.atan(::Math.tan(deg2rad(@fov) / 2.0) * 0.75) * 180.0 / ::Math::PI
75
+ end
76
+
77
+ # Actual horizontal FOV for a given aspect (follows from fov_y)
78
+ def fov_x(aspect)
79
+ 2.0 * ::Math.atan(::Math.tan(deg2rad(fov_y) / 2.0) * aspect) * 180.0 / ::Math::PI
80
+ end
81
+
82
+ # R_SetFrustum: 4 view-frustum planes as [normal, dist] pairs, where a
83
+ # point is inside when normal.dot(point) - dist >= 0.
84
+ def frustum_planes(aspect)
85
+ vecs = quake_angle_vectors
86
+ forward = vecs.fetch(:forward)
87
+ right = vecs.fetch(:right)
88
+ up = vecs.fetch(:up)
89
+
90
+ fx = fov_x(aspect)
91
+ fy = fov_y
92
+ normals = [
93
+ rotate_around_axis(forward, up, -(90.0 - fx / 2.0)),
94
+ rotate_around_axis(forward, up, 90.0 - fx / 2.0),
95
+ rotate_around_axis(forward, right, 90.0 - fy / 2.0),
96
+ rotate_around_axis(forward, right, -(90.0 - fy / 2.0))
97
+ ]
98
+ normals.map { |normal| [normal, normal.dot(@position)] }
76
99
  end
77
100
 
78
101
  def apply_gl
@@ -85,6 +108,7 @@ module Quake
85
108
  GL.Rotatef(-90.0, 1.0, 0.0, 0.0) # Z-up -> Y-up
86
109
  GL.Rotatef(90.0, 0.0, 0.0, 1.0) # X-forward -> GL
87
110
 
111
+ GL.Rotatef(-@roll, 1.0, 0.0, 0.0)
88
112
  GL.Rotatef(-@pitch, 0.0, 1.0, 0.0)
89
113
  GL.Rotatef(-@yaw, 0.0, 0.0, 1.0)
90
114
  GL.Translatef(-@position.x, -@position.y, -@position.z)
@@ -95,5 +119,38 @@ module Quake
95
119
  def deg2rad(deg)
96
120
  deg * ::Math::PI / 180.0
97
121
  end
122
+
123
+ # Rodrigues rotation of v around unit axis by degrees
124
+ def rotate_around_axis(v, axis, degrees)
125
+ rad = deg2rad(degrees)
126
+ cos = ::Math.cos(rad)
127
+ sin = ::Math.sin(rad)
128
+ cross = axis.cross(v)
129
+ dot = axis.dot(v)
130
+ Math::Vec3.new(
131
+ v.x * cos + cross.x * sin + axis.x * dot * (1.0 - cos),
132
+ v.y * cos + cross.y * sin + axis.y * dot * (1.0 - cos),
133
+ v.z * cos + cross.z * sin + axis.z * dot * (1.0 - cos)
134
+ )
135
+ end
136
+
137
+ def quake_angle_vectors
138
+ sy = ::Math.sin(deg2rad(@yaw))
139
+ cy = ::Math.cos(deg2rad(@yaw))
140
+ sp = ::Math.sin(deg2rad(@pitch))
141
+ cp = ::Math.cos(deg2rad(@pitch))
142
+ sr = ::Math.sin(deg2rad(@roll))
143
+ cr = ::Math.cos(deg2rad(@roll))
144
+
145
+ {
146
+ forward: Math::Vec3.new(cp * cy, cp * sy, -sp),
147
+ right: Math::Vec3.new((-sr * sp * cy) + (cr * sy),
148
+ (-sr * sp * sy) - (cr * cy),
149
+ -sr * cp),
150
+ up: Math::Vec3.new((cr * sp * cy) + (sr * sy),
151
+ (cr * sp * sy) - (sr * cy),
152
+ cr * cp)
153
+ }
154
+ end
98
155
  end
99
156
  end