doom 0.8.0 → 0.11.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.
- checksums.yaml +4 -4
- data/LICENSE +17 -0
- data/README.md +65 -4
- data/bin/doom +134 -24
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +239 -153
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +22 -39
- data/lib/doom/game/item_pickup.rb +87 -75
- data/lib/doom/game/menu.rb +90 -85
- data/lib/doom/game/monster_ai.rb +150 -136
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +38 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +169 -93
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +33 -58
- data/lib/doom/game/state_hash.rb +125 -0
- data/lib/doom/game/ticcmd.rb +61 -0
- data/lib/doom/game/world.rb +420 -0
- data/lib/doom/map/data.rb +95 -0
- data/lib/doom/net/client.rb +204 -0
- data/lib/doom/net/desync_monitor.rb +101 -0
- data/lib/doom/net/game_server.rb +232 -0
- data/lib/doom/net/lockstep.rb +170 -0
- data/lib/doom/net/protocol.rb +350 -0
- data/lib/doom/net/session.rb +282 -0
- data/lib/doom/net/transport.rb +110 -0
- data/lib/doom/platform/gosu_window.rb +604 -832
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +5 -3
- data/lib/doom/render/hardware_renderer.rb +547 -0
- data/lib/doom/render/ray_tracing/bvh.rb +103 -0
- data/lib/doom/render/ray_tracing/material_state.rb +66 -0
- data/lib/doom/render/ray_tracing/texture_atlas.rb +78 -0
- data/lib/doom/render/ray_tracing_renderer.rb +940 -0
- data/lib/doom/render/renderer.rb +466 -440
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +7 -7
- data/lib/doom/render/spinel_native/kernel.rb +780 -0
- data/lib/doom/render/spinel_native_renderer.rb +162 -0
- data/lib/doom/render/status_bar.rb +14 -10
- data/lib/doom/render/weapon_renderer.rb +9 -10
- data/lib/doom/render/world_mesh.rb +270 -0
- data/lib/doom/render/zbuffer_renderer.rb +151 -0
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/flat.rb +1 -1
- data/lib/doom/wad/hud_graphics.rb +7 -3
- data/lib/doom/wad/palette.rb +3 -3
- data/lib/doom/wad/patch.rb +1 -1
- data/lib/doom/wad/reader.rb +12 -15
- data/lib/doom/wad/sound.rb +14 -6
- data/lib/doom/wad/sprite.rb +36 -35
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +197 -16
- metadata +94 -7
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "spinel_native/kernel"
|
|
4
|
+
|
|
5
|
+
module Doom
|
|
6
|
+
module Render
|
|
7
|
+
# Runs the classic software renderer's hot path (BSP -> textured walls ->
|
|
8
|
+
# floors/ceilings -> sky) through spinel_native. The pixel-producing code
|
|
9
|
+
# lives in Doom::Render::SpinelNative::Kernel as native methods that mirror
|
|
10
|
+
# lib/doom/render/renderer.rb one-to-one; this class only flattens the map,
|
|
11
|
+
# textures and flats into the plain Integer/Float arrays the kernel boundary
|
|
12
|
+
# accepts, then calls it once per frame and hands back the framebuffer.
|
|
13
|
+
#
|
|
14
|
+
# The same kernel methods run as plain Ruby when SPINEL_NATIVE=off, so the
|
|
15
|
+
# port can be verified against the reference renderer before compiling.
|
|
16
|
+
class SpinelNativeRenderer
|
|
17
|
+
SCREEN_WIDTH = 320
|
|
18
|
+
SCREEN_HEIGHT = 240
|
|
19
|
+
EYE_HEIGHT = 41
|
|
20
|
+
|
|
21
|
+
attr_reader :palette
|
|
22
|
+
|
|
23
|
+
def initialize(map, palette, colormap, textures, flats)
|
|
24
|
+
@map = map
|
|
25
|
+
@palette = palette
|
|
26
|
+
flatten_all(map, colormap, textures, flats)
|
|
27
|
+
upload
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Upload the flattened map/textures into the kernel's state once.
|
|
31
|
+
def upload
|
|
32
|
+
SpinelNative::Kernel.load_map(
|
|
33
|
+
@nodes_f, @verts, @segs, @subs, @lines, @sides, @secs,
|
|
34
|
+
@wpx, @wtab, @fpx, @cmap, @node_count, @sky_tex
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Render one frame from the given eye pose. Returns a 76800-int framebuffer
|
|
39
|
+
# of palette indices (row-major, 320x240).
|
|
40
|
+
def render(px, py, angle_rad)
|
|
41
|
+
pz = floor_height_at(px, py) + EYE_HEIGHT
|
|
42
|
+
SpinelNative::Kernel.render(px.round, py.round, pz, angle_rad)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Render with an explicit eye height (the game supplies player_z), for
|
|
46
|
+
# integration into the real Renderer.
|
|
47
|
+
def render_at(px, py, pz, angle_rad)
|
|
48
|
+
SpinelNative::Kernel.render(px.round, py.round, pz.round, angle_rad)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Wall-pass outputs from the last render, for the Ruby sprite pass.
|
|
52
|
+
def ceiling_clip = SpinelNative::Kernel.get_cclip
|
|
53
|
+
def floor_clip = SpinelNative::Kernel.get_fclip
|
|
54
|
+
def wall_depth = SpinelNative::Kernel.get_wdepth
|
|
55
|
+
|
|
56
|
+
# Eye height follows the floor under the player (classic pz = floorz + 41).
|
|
57
|
+
def floor_height_at(x, y)
|
|
58
|
+
ni = @node_count - 1
|
|
59
|
+
while (ni & 0x8000).zero?
|
|
60
|
+
b = ni * 6
|
|
61
|
+
side = (x.to_f - @nodes_f[b]) * @nodes_f[b + 3] >= (y.to_f - @nodes_f[b + 1]) * @nodes_f[b + 2] ? 0 : 1
|
|
62
|
+
ni = (side.zero? ? @nodes_f[b + 4] : @nodes_f[b + 5]).to_i
|
|
63
|
+
end
|
|
64
|
+
first = @subs[(ni & 0x7FFF) * 2]
|
|
65
|
+
sdi = @lines[@segs[first * 5 + 2] * 3 + (@segs[first * 5 + 3].zero? ? 0 : 1)]
|
|
66
|
+
@secs[@sides[sdi * 6] * 6]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def flatten_all(map, colormap, textures, flats)
|
|
72
|
+
# --- wall textures (composited, column-major) + name->index ------------
|
|
73
|
+
@wall_index = {}
|
|
74
|
+
@wall_names = []
|
|
75
|
+
map.sidedefs.each do |sd|
|
|
76
|
+
add_wall(sd.upper_texture); add_wall(sd.lower_texture); add_wall(sd.middle_texture)
|
|
77
|
+
end
|
|
78
|
+
@sky_tex = add_wall("SKY1")
|
|
79
|
+
|
|
80
|
+
@wpx = []
|
|
81
|
+
@wtab = []
|
|
82
|
+
@wall_names.each do |name|
|
|
83
|
+
comp = textures[name]
|
|
84
|
+
if comp.nil?
|
|
85
|
+
@wtab.push(@wpx.length, 1, 1); @wpx.push(-1); next
|
|
86
|
+
end
|
|
87
|
+
@wtab.push(@wpx.length, comp.width, comp.height)
|
|
88
|
+
tx = 0
|
|
89
|
+
while tx < comp.width
|
|
90
|
+
col = comp.column_pixels(tx)
|
|
91
|
+
ty = 0
|
|
92
|
+
while ty < comp.height
|
|
93
|
+
c = col[ty]
|
|
94
|
+
@wpx.push(c.nil? ? -1 : c) # -1 = transparent texel (skipped)
|
|
95
|
+
ty += 1
|
|
96
|
+
end
|
|
97
|
+
tx += 1
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# --- flats (64x64 row-major) + name->index ----------------------------
|
|
102
|
+
@flat_index = {}
|
|
103
|
+
@fpx = []
|
|
104
|
+
idx = 0
|
|
105
|
+
flats.each do |fl|
|
|
106
|
+
@flat_index[fl.name] = idx
|
|
107
|
+
@fpx.concat(fl.pixels)
|
|
108
|
+
idx += 1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# --- colormap (flat 34*256) ------------------------------------------
|
|
112
|
+
@cmap = colormap.respond_to?(:maps) ? flatten_colormap(colormap.maps) : colormap
|
|
113
|
+
|
|
114
|
+
# --- geometry ---------------------------------------------------------
|
|
115
|
+
@nodes_f = []
|
|
116
|
+
map.nodes.each { |n| @nodes_f.push(n.x.to_f, n.y.to_f, n.dx.to_f, n.dy.to_f, n.child_right.to_f, n.child_left.to_f) }
|
|
117
|
+
@verts = []
|
|
118
|
+
map.vertices.each { |v| @verts.push(v.x, v.y) }
|
|
119
|
+
@segs = []
|
|
120
|
+
map.segs.each { |s| @segs.push(s.v1, s.v2, s.linedef, s.direction, s.offset) }
|
|
121
|
+
@subs = []
|
|
122
|
+
map.subsectors.each { |s| @subs.push(s.first_seg, s.seg_count) }
|
|
123
|
+
@lines = []
|
|
124
|
+
map.linedefs.each { |l| @lines.push(l.sidedef_right, l.sidedef_left, l.flags) }
|
|
125
|
+
@sides = []
|
|
126
|
+
map.sidedefs.each do |sd|
|
|
127
|
+
@sides.push(sd.sector, wall_idx(sd.upper_texture), wall_idx(sd.lower_texture),
|
|
128
|
+
wall_idx(sd.middle_texture), sd.x_offset, sd.y_offset)
|
|
129
|
+
end
|
|
130
|
+
@secs = []
|
|
131
|
+
map.sectors.each do |s|
|
|
132
|
+
@secs.push(s.floor_height, s.ceiling_height, s.light_level,
|
|
133
|
+
flat_idx(s.floor_texture), flat_idx(s.ceiling_texture),
|
|
134
|
+
s.ceiling_texture == "F_SKY1" ? 1 : 0,
|
|
135
|
+
s.floor_texture == "F_SKY1" ? 1 : 0)
|
|
136
|
+
end
|
|
137
|
+
@node_count = map.nodes.length
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def flatten_colormap(maps)
|
|
141
|
+
return maps if maps.first.is_a?(Integer) # already flat
|
|
142
|
+
|
|
143
|
+
maps.flatten
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def add_wall(name)
|
|
147
|
+
return -1 if name.nil? || name.empty? || name == "-"
|
|
148
|
+
|
|
149
|
+
key = name.upcase
|
|
150
|
+
@wall_index[key] ||= begin
|
|
151
|
+
@wall_names << key
|
|
152
|
+
@wall_names.length - 1
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
alias wall_idx add_wall
|
|
156
|
+
|
|
157
|
+
def flat_idx(name)
|
|
158
|
+
@flat_index[name] || @flat_index[name.to_s.upcase] || 0
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -28,11 +28,14 @@ module Doom
|
|
|
28
28
|
# Small ammo counts (right side of status bar)
|
|
29
29
|
SMALL_AMMO_X = 288 # Current ammo X
|
|
30
30
|
SMALL_MAX_X = 314 # Max ammo X
|
|
31
|
-
SMALL_AMMO_Y = [5, 11, 23, 17]
|
|
31
|
+
SMALL_AMMO_Y = [5, 11, 23, 17] # Bullets, Shells, Cells, Rockets (relative to bar)
|
|
32
32
|
|
|
33
33
|
NUM_WIDTH = 14 # Width of large digit
|
|
34
34
|
SMALL_NUM_WIDTH = 4 # Width of small digit
|
|
35
35
|
|
|
36
|
+
attr_reader :gfx
|
|
37
|
+
attr_writer :player # Re-pointed on world rebind (map change, netgame resync)
|
|
38
|
+
|
|
36
39
|
def initialize(hud_graphics, player_state)
|
|
37
40
|
@gfx = hud_graphics
|
|
38
41
|
@player = player_state
|
|
@@ -77,10 +80,10 @@ module Doom
|
|
|
77
80
|
def update
|
|
78
81
|
# Cycle face animation
|
|
79
82
|
@face_timer += 1
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
return unless @face_timer > 15 # Change face every ~0.5 seconds
|
|
84
|
+
|
|
85
|
+
@face_timer = 0
|
|
86
|
+
@face_index = (@face_index + 1) % 3
|
|
84
87
|
end
|
|
85
88
|
|
|
86
89
|
private
|
|
@@ -101,7 +104,7 @@ module Doom
|
|
|
101
104
|
draw_y = y + sy
|
|
102
105
|
next if draw_y < 0 || draw_y >= SCREEN_HEIGHT
|
|
103
106
|
|
|
104
|
-
framebuffer[draw_y * SCREEN_WIDTH + draw_x] = color
|
|
107
|
+
framebuffer[(draw_y * SCREEN_WIDTH) + draw_x] = color
|
|
105
108
|
end
|
|
106
109
|
end
|
|
107
110
|
end
|
|
@@ -137,13 +140,13 @@ module Doom
|
|
|
137
140
|
def draw_arms(framebuffer)
|
|
138
141
|
# Weapon numbers 2-7 in a 3x2 grid
|
|
139
142
|
6.times do |i|
|
|
140
|
-
weapon_num = i + 2
|
|
143
|
+
weapon_num = i + 2 # weapons 2-7
|
|
141
144
|
owned = @player.has_weapons[weapon_num]
|
|
142
145
|
digit = owned ? @gfx.yellow_numbers[weapon_num] : @gfx.grey_numbers[weapon_num]
|
|
143
146
|
next unless digit
|
|
144
147
|
|
|
145
|
-
x = ARMS_X + (i % 3) * ARMS_XSPACE
|
|
146
|
-
y = STATUS_BAR_Y + ARMS_Y + (i / 3) * ARMS_YSPACE
|
|
148
|
+
x = ARMS_X + ((i % 3) * ARMS_XSPACE)
|
|
149
|
+
y = STATUS_BAR_Y + ARMS_Y + ((i / 3) * ARMS_YSPACE)
|
|
147
150
|
draw_sprite(framebuffer, digit, x, y)
|
|
148
151
|
end
|
|
149
152
|
end
|
|
@@ -161,6 +164,7 @@ module Doom
|
|
|
161
164
|
end
|
|
162
165
|
|
|
163
166
|
return unless face
|
|
167
|
+
|
|
164
168
|
draw_sprite(framebuffer, face, FACE_X, STATUS_BAR_Y + FACE_Y)
|
|
165
169
|
end
|
|
166
170
|
|
|
@@ -177,6 +181,7 @@ module Doom
|
|
|
177
181
|
|
|
178
182
|
def draw_small_number_right(framebuffer, value, right_x, y)
|
|
179
183
|
return unless value
|
|
184
|
+
|
|
180
185
|
str = value.to_i.to_s
|
|
181
186
|
current_x = right_x
|
|
182
187
|
str.reverse.each_char do |char|
|
|
@@ -190,7 +195,6 @@ module Doom
|
|
|
190
195
|
|
|
191
196
|
def draw_keys(framebuffer)
|
|
192
197
|
key_x = KEYS_X
|
|
193
|
-
key_spacing = 10
|
|
194
198
|
|
|
195
199
|
# Blue keys (top row)
|
|
196
200
|
if @player.keys[:blue_card]
|
|
@@ -7,12 +7,11 @@ module Doom
|
|
|
7
7
|
# Weapon is rendered above the status bar
|
|
8
8
|
WEAPON_AREA_HEIGHT = SCREEN_HEIGHT - StatusBar::STATUS_BAR_HEIGHT
|
|
9
9
|
|
|
10
|
-
# Chocolate Doom R_DrawPSprite weapon
|
|
11
|
-
# centery = viewheight/2 (view area excluding status bar)
|
|
12
|
-
# texturemid = centery - (WEAPONTOP - spritetopoffset)
|
|
13
|
-
# dc_yl = centery - texturemid (first visible row)
|
|
10
|
+
# Chocolate Doom R_DrawPSprite weapon top: dc_yl = WEAPONTOP - spritetopoffset
|
|
14
11
|
WEAPONTOP = 32
|
|
15
|
-
|
|
12
|
+
|
|
13
|
+
attr_reader :gfx
|
|
14
|
+
attr_writer :player # Re-pointed on world rebind (map change, netgame resync)
|
|
16
15
|
|
|
17
16
|
def initialize(hud_graphics, player_state)
|
|
18
17
|
@gfx = hud_graphics
|
|
@@ -47,9 +46,9 @@ module Doom
|
|
|
47
46
|
draw_weapon_sprite(framebuffer, sprite, x, y)
|
|
48
47
|
|
|
49
48
|
# Draw muzzle flash only on the first fire frame (the actual shot)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
return unless @player.attacking && @player.attack_frame == 0
|
|
50
|
+
|
|
51
|
+
draw_muzzle_flash(framebuffer, weapon_name)
|
|
53
52
|
end
|
|
54
53
|
|
|
55
54
|
private
|
|
@@ -68,12 +67,12 @@ module Doom
|
|
|
68
67
|
next if draw_x < 0 || draw_x >= SCREEN_WIDTH
|
|
69
68
|
|
|
70
69
|
column.each_with_index do |color, sy|
|
|
71
|
-
next unless color
|
|
70
|
+
next unless color # Skip transparent pixels
|
|
72
71
|
|
|
73
72
|
draw_y = base_y + sy
|
|
74
73
|
next if draw_y < 0 || draw_y > max_y
|
|
75
74
|
|
|
76
|
-
framebuffer[draw_y * SCREEN_WIDTH + draw_x] = color
|
|
75
|
+
framebuffer[(draw_y * SCREEN_WIDTH) + draw_x] = color
|
|
77
76
|
end
|
|
78
77
|
end
|
|
79
78
|
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Render
|
|
5
|
+
# Converts Doom's sector/subsector representation into ordinary triangles.
|
|
6
|
+
# Subsector polygons are convex by construction, so a fan is sufficient.
|
|
7
|
+
class WorldMesh
|
|
8
|
+
Triangle = Struct.new(:vertices, :uvs, :normal, :light, :material, :masked)
|
|
9
|
+
|
|
10
|
+
attr_reader :triangles
|
|
11
|
+
|
|
12
|
+
def initialize(map, textures = nil)
|
|
13
|
+
@map = map
|
|
14
|
+
@textures = textures
|
|
15
|
+
@triangles = []
|
|
16
|
+
build_planes
|
|
17
|
+
build_walls
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def build_planes
|
|
23
|
+
bsp_leaf_polygons.each do |subsector_index, points|
|
|
24
|
+
subsector = @map.subsectors[subsector_index]
|
|
25
|
+
segs = @map.segs[subsector.first_seg, subsector.seg_count]
|
|
26
|
+
next if segs.nil? || segs.empty?
|
|
27
|
+
|
|
28
|
+
# BSP leaves partition the entire map bounding box, including solid
|
|
29
|
+
# void behind one-sided walls. Segs are directed with their front
|
|
30
|
+
# sector on the right; clipping by each one trims the leaf to the
|
|
31
|
+
# actual playable subsector surface.
|
|
32
|
+
points = clip_to_segs(points, segs)
|
|
33
|
+
|
|
34
|
+
sector = segs.lazy.map { |seg| sector_for_seg(seg) }.find(&:itself)
|
|
35
|
+
next unless sector
|
|
36
|
+
|
|
37
|
+
next if points.size < 3
|
|
38
|
+
|
|
39
|
+
(1...(points.size - 1)).each do |i|
|
|
40
|
+
add_triangle(points[0], points[i], points[i + 1], sector.floor_height,
|
|
41
|
+
[0.0, 0.0, 1.0], sector.light_level, sector.floor_texture)
|
|
42
|
+
unless sector.ceiling_texture == 'F_SKY1'
|
|
43
|
+
add_triangle(points[0], points[i + 1], points[i], sector.ceiling_height,
|
|
44
|
+
[0.0, 0.0, -1.0], sector.light_level, sector.ceiling_texture)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clip_to_segs(points, segs)
|
|
51
|
+
segs.reduce(points) do |polygon, seg|
|
|
52
|
+
break [] if polygon.size < 3
|
|
53
|
+
|
|
54
|
+
a = @map.vertices[seg.v1]
|
|
55
|
+
b = @map.vertices[seg.v2]
|
|
56
|
+
dx = b.x - a.x
|
|
57
|
+
dy = b.y - a.y
|
|
58
|
+
clip_by_values(polygon) { |point| ((point.x - a.x) * dy) - ((point.y - a.y) * dx) }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def clip_by_values(polygon)
|
|
63
|
+
output = []
|
|
64
|
+
polygon.each_with_index do |current, index|
|
|
65
|
+
previous = polygon[index - 1]
|
|
66
|
+
current_value = yield(current)
|
|
67
|
+
previous_value = yield(previous)
|
|
68
|
+
current_inside = current_value >= -1e-7
|
|
69
|
+
previous_inside = previous_value >= -1e-7
|
|
70
|
+
if current_inside != previous_inside
|
|
71
|
+
t = previous_value / (previous_value - current_value).to_f
|
|
72
|
+
output << Map::Vertex.new(
|
|
73
|
+
previous.x + ((current.x - previous.x) * t),
|
|
74
|
+
previous.y + ((current.y - previous.y) * t)
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
output << current if current_inside
|
|
78
|
+
end
|
|
79
|
+
output
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Reconstruct exact convex subsector cells by carrying the map bounding
|
|
83
|
+
# polygon through the BSP and clipping it at every partition. Seg
|
|
84
|
+
# endpoints alone do not contain the artificial BSP split edges.
|
|
85
|
+
def bsp_leaf_polygons
|
|
86
|
+
xs = @map.vertices.map(&:x)
|
|
87
|
+
ys = @map.vertices.map(&:y)
|
|
88
|
+
padding = 1.0
|
|
89
|
+
root_polygon = [
|
|
90
|
+
[xs.min - padding, ys.min - padding],
|
|
91
|
+
[xs.max + padding, ys.min - padding],
|
|
92
|
+
[xs.max + padding, ys.max + padding],
|
|
93
|
+
[xs.min - padding, ys.max + padding]
|
|
94
|
+
]
|
|
95
|
+
leaves = {}
|
|
96
|
+
visit_bsp(@map.nodes.size - 1, root_polygon, leaves)
|
|
97
|
+
leaves
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def visit_bsp(index, polygon, leaves)
|
|
101
|
+
return if polygon.size < 3
|
|
102
|
+
|
|
103
|
+
if (index & Map::Node::SUBSECTOR_FLAG) != 0
|
|
104
|
+
leaves[index & ~Map::Node::SUBSECTOR_FLAG] = polygon.map { |x, y| Map::Vertex.new(x, y) }
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
node = @map.nodes[index]
|
|
109
|
+
visit_bsp(node.child_right, clip_polygon(polygon, node, true), leaves)
|
|
110
|
+
visit_bsp(node.child_left, clip_polygon(polygon, node, false), leaves)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def clip_polygon(polygon, node, keep_right)
|
|
114
|
+
output = []
|
|
115
|
+
polygon.each_with_index do |current, index|
|
|
116
|
+
previous = polygon[index - 1]
|
|
117
|
+
current_side = partition_value(current, node)
|
|
118
|
+
previous_side = partition_value(previous, node)
|
|
119
|
+
current_inside = keep_right ? current_side >= -1e-7 : current_side <= 1e-7
|
|
120
|
+
previous_inside = keep_right ? previous_side >= -1e-7 : previous_side <= 1e-7
|
|
121
|
+
|
|
122
|
+
if current_inside != previous_inside
|
|
123
|
+
t = previous_side / (previous_side - current_side).to_f
|
|
124
|
+
output << [previous[0] + ((current[0] - previous[0]) * t),
|
|
125
|
+
previous[1] + ((current[1] - previous[1]) * t)]
|
|
126
|
+
end
|
|
127
|
+
output << current if current_inside
|
|
128
|
+
end
|
|
129
|
+
output
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def partition_value(point, node)
|
|
133
|
+
((point[0] - node.x) * node.dy) - ((point[1] - node.y) * node.dx)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def build_walls
|
|
137
|
+
@map.linedefs.each do |line|
|
|
138
|
+
right = sidedef(line.sidedef_right)
|
|
139
|
+
left = sidedef(line.sidedef_left)
|
|
140
|
+
next unless right || left
|
|
141
|
+
|
|
142
|
+
front = right && @map.sectors[right.sector]
|
|
143
|
+
back = left && @map.sectors[left.sector]
|
|
144
|
+
a = @map.vertices[line.v1]
|
|
145
|
+
b = @map.vertices[line.v2]
|
|
146
|
+
|
|
147
|
+
if front && back
|
|
148
|
+
# Doom's sky hack: adjacent sky ceilings are one continuous
|
|
149
|
+
# infinite sky even when their numeric heights differ. Emitting an
|
|
150
|
+
# upper wall here creates the conspicuous tower at outdoor edges.
|
|
151
|
+
both_sky = front.ceiling_texture == 'F_SKY1' && back.ceiling_texture == 'F_SKY1'
|
|
152
|
+
if !both_sky && front.ceiling_height > back.ceiling_height
|
|
153
|
+
v_top = right.y_offset
|
|
154
|
+
v_top += back.ceiling_height - front.ceiling_height unless line.upper_unpegged?
|
|
155
|
+
wall_quad(a, b, back.ceiling_height, front.ceiling_height,
|
|
156
|
+
front.light_level, right.upper_texture,
|
|
157
|
+
u_offset: right.x_offset, v_top: v_top)
|
|
158
|
+
elsif !both_sky && back.ceiling_height > front.ceiling_height
|
|
159
|
+
v_top = left.y_offset
|
|
160
|
+
v_top += front.ceiling_height - back.ceiling_height unless line.upper_unpegged?
|
|
161
|
+
wall_quad(a, b, front.ceiling_height, back.ceiling_height,
|
|
162
|
+
back.light_level, left.upper_texture, flip: true,
|
|
163
|
+
u_offset: left.x_offset, v_top: v_top)
|
|
164
|
+
end
|
|
165
|
+
if back.floor_height > front.floor_height
|
|
166
|
+
v_top = right.y_offset
|
|
167
|
+
v_top += front.ceiling_height - back.floor_height if line.lower_unpegged?
|
|
168
|
+
wall_quad(a, b, front.floor_height, back.floor_height,
|
|
169
|
+
front.light_level, right.lower_texture,
|
|
170
|
+
u_offset: right.x_offset, v_top: v_top)
|
|
171
|
+
elsif front.floor_height > back.floor_height
|
|
172
|
+
v_top = left.y_offset
|
|
173
|
+
v_top += back.ceiling_height - front.floor_height if line.lower_unpegged?
|
|
174
|
+
wall_quad(a, b, back.floor_height, front.floor_height,
|
|
175
|
+
back.light_level, left.lower_texture, flip: true,
|
|
176
|
+
u_offset: left.x_offset, v_top: v_top)
|
|
177
|
+
end
|
|
178
|
+
opening_bottom = [front.floor_height, back.floor_height].max
|
|
179
|
+
opening_top = [front.ceiling_height, back.ceiling_height].min
|
|
180
|
+
if middle_material?(right)
|
|
181
|
+
masked_middle(a, b, opening_bottom, opening_top, front, right, line)
|
|
182
|
+
elsif middle_material?(left)
|
|
183
|
+
masked_middle(a, b, opening_bottom, opening_top, back, left, line, flip: true)
|
|
184
|
+
end
|
|
185
|
+
else
|
|
186
|
+
sector = front || back
|
|
187
|
+
side = right || left
|
|
188
|
+
v_top = side.y_offset
|
|
189
|
+
v_top -= sector.ceiling_height - sector.floor_height if line.lower_unpegged?
|
|
190
|
+
wall_quad(a, b, sector.floor_height, sector.ceiling_height,
|
|
191
|
+
sector.light_level, side.middle_texture, flip: !right,
|
|
192
|
+
u_offset: side.x_offset, v_top: v_top)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def middle_material?(side)
|
|
198
|
+
side && !side.middle_texture.nil? && !side.middle_texture.empty? && side.middle_texture != '-'
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def masked_middle(a, b, opening_bottom, opening_top, sector, side, line, flip: false)
|
|
202
|
+
material = side.middle_texture
|
|
203
|
+
return if material.nil? || material.empty? || material == '-'
|
|
204
|
+
|
|
205
|
+
texture = @textures&.[](material)
|
|
206
|
+
texture_height = texture&.height || (opening_top - opening_bottom)
|
|
207
|
+
texture_top = if line.lower_unpegged?
|
|
208
|
+
opening_bottom + texture_height + side.y_offset
|
|
209
|
+
else
|
|
210
|
+
opening_top + side.y_offset
|
|
211
|
+
end
|
|
212
|
+
bottom = [opening_bottom, texture_top - texture_height].max
|
|
213
|
+
top = [opening_top, texture_top].min
|
|
214
|
+
return unless top > bottom
|
|
215
|
+
|
|
216
|
+
wall_quad(a, b, bottom, top, sector.light_level, material, flip: flip,
|
|
217
|
+
u_offset: side.x_offset, v_top: texture_top - top, masked: true)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def wall_quad(a, b, bottom, top, light, material, flip: false, u_offset: 0, v_top: 0, masked: false)
|
|
221
|
+
return unless top > bottom
|
|
222
|
+
|
|
223
|
+
a, b = b, a if flip
|
|
224
|
+
|
|
225
|
+
dx = b.x - a.x
|
|
226
|
+
dy = b.y - a.y
|
|
227
|
+
length = Math.hypot(dx, dy)
|
|
228
|
+
return if length.zero?
|
|
229
|
+
|
|
230
|
+
normal = [dy / length, -dx / length, 0.0]
|
|
231
|
+
v0 = [a.x.to_f, a.y.to_f, bottom.to_f]
|
|
232
|
+
v1 = [b.x.to_f, b.y.to_f, bottom.to_f]
|
|
233
|
+
v2 = [b.x.to_f, b.y.to_f, top.to_f]
|
|
234
|
+
v3 = [a.x.to_f, a.y.to_f, top.to_f]
|
|
235
|
+
# UVs remain in Doom world/texel units. The GPU renderer normalizes
|
|
236
|
+
# them by the actual material dimensions (not every wall is 64 px).
|
|
237
|
+
u0 = u_offset.to_f
|
|
238
|
+
u1 = u0 + length
|
|
239
|
+
v0_tex = v_top.to_f + (top - bottom)
|
|
240
|
+
v1_tex = v_top.to_f
|
|
241
|
+
@triangles << Triangle.new([v0, v1, v2], [[u0, v0_tex], [u1, v0_tex], [u1, v1_tex]], normal, light, material,
|
|
242
|
+
masked)
|
|
243
|
+
@triangles << Triangle.new([v0, v2, v3], [[u0, v0_tex], [u1, v1_tex], [u0, v1_tex]], normal, light, material,
|
|
244
|
+
masked)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def add_triangle(a, b, c, z, normal, light, material)
|
|
248
|
+
area = ((b.x - a.x) * (c.y - a.y)) - ((b.y - a.y) * (c.x - a.x))
|
|
249
|
+
return if area.abs < 1e-6
|
|
250
|
+
|
|
251
|
+
vertices = [a, b, c].map { |point| [point.x.to_f, point.y.to_f, z.to_f] }
|
|
252
|
+
uvs = [a, b, c].map { |point| [point.x.to_f, -point.y.to_f] }
|
|
253
|
+
@triangles << Triangle.new(vertices, uvs, normal, light, material)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def sector_for_seg(seg)
|
|
257
|
+
line = @map.linedefs[seg.linedef]
|
|
258
|
+
index = seg.direction.zero? ? line.sidedef_right : line.sidedef_left
|
|
259
|
+
side = sidedef(index)
|
|
260
|
+
side && @map.sectors[side.sector]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def sidedef(index)
|
|
264
|
+
return nil unless index && index >= 0 && index < @map.sidedefs.size
|
|
265
|
+
|
|
266
|
+
@map.sidedefs[index]
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|