Class: Doom::Render::Renderer

Inherits:
Object
  • Object
show all
Defined in:
lib/doom/render/renderer.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(wad, map, textures, palette, colormap, flats, sprites = nil) ⇒ Renderer

Returns a new instance of Renderer.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/doom/render/renderer.rb', line 51

def initialize(wad, map, textures, palette, colormap, flats, sprites = nil)
  @wad = wad
  @map = map
  @textures = textures
  @palette = palette
  @colormap = colormap
  @flats = flats.to_h { |f| [f.name, f] }
  @sprites = sprites

  @framebuffer = Array.new(SCREEN_WIDTH * SCREEN_HEIGHT, 0)

  @player_x = 0.0
  @player_y = 0.0
  @player_z = 41.0
  @player_angle = 0.0

  # Projection constant - distance to projection plane
  @projection = HALF_WIDTH / Math.tan(FOV * Math::PI / 360.0)

  # Clipping arrays
  @ceiling_clip = Array.new(SCREEN_WIDTH, -1)
  @floor_clip = Array.new(SCREEN_WIDTH, SCREEN_HEIGHT)

  # Sprite clip arrays (copy of wall clips for sprite clipping)
  @sprite_ceiling_clip = Array.new(SCREEN_WIDTH, -1)
  @sprite_floor_clip = Array.new(SCREEN_WIDTH, SCREEN_HEIGHT)

  # Wall depth array - tracks distance to nearest wall at each column
  @wall_depth = Array.new(SCREEN_WIDTH, Float::INFINITY)
end

Instance Attribute Details

#cos_angleObject (readonly)

Returns the value of attribute cos_angle.



82
83
84
# File 'lib/doom/render/renderer.rb', line 82

def cos_angle
  @cos_angle
end

#framebufferObject (readonly)

Returns the value of attribute framebuffer.



49
50
51
# File 'lib/doom/render/renderer.rb', line 49

def framebuffer
  @framebuffer
end

#player_xObject (readonly)

Returns the value of attribute player_x.



82
83
84
# File 'lib/doom/render/renderer.rb', line 82

def player_x
  @player_x
end

#player_yObject (readonly)

Returns the value of attribute player_y.



82
83
84
# File 'lib/doom/render/renderer.rb', line 82

def player_y
  @player_y
end

#player_zObject (readonly)

Returns the value of attribute player_z.



82
83
84
# File 'lib/doom/render/renderer.rb', line 82

def player_z
  @player_z
end

#sin_angleObject (readonly)

Returns the value of attribute sin_angle.



82
83
84
# File 'lib/doom/render/renderer.rb', line 82

def sin_angle
  @sin_angle
end

Instance Method Details

#check_plane(plane, start_x, stop_x) ⇒ Object

R_CheckPlane equivalent - check if columns in range are already marked If overlap exists, create a new visplane; otherwise update minx/maxx



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/doom/render/renderer.rb', line 325

def check_plane(plane, start_x, stop_x)
  return plane unless plane

  # Calculate intersection and union of column ranges
  if start_x < plane.minx
    intrl = plane.minx
    unionl = start_x
  else
    unionl = plane.minx
    intrl = start_x
  end

  if stop_x > plane.maxx
    intrh = plane.maxx
    unionh = stop_x
  else
    unionh = plane.maxx
    intrh = stop_x
  end

  # Check if any column in intersection range is already marked
  # A column is marked if top[x] <= bottom[x] (valid range)
  overlap = false
  (intrl..intrh).each do |x|
    next if x < 0 || x >= SCREEN_WIDTH
    if plane.top[x] <= plane.bottom[x]
      overlap = true
      break
    end
  end

  if !overlap
    # No overlap - reuse same visplane with expanded range
    plane.minx = unionl if unionl < plane.minx
    plane.maxx = unionh if unionh > plane.maxx
    return plane
  end

  # Overlap detected - create new visplane with same properties
  new_plane = Visplane.new(
    plane.sector,
    plane.height,
    plane.texture,
    plane.light_level,
    plane.is_ceiling
  )
  new_plane.minx = start_x
  new_plane.maxx = stop_x
  @visplanes << new_plane
  new_plane
end

#draw_all_visplanesObject

Render all visplanes after BSP traversal (R_DrawPlanes in Chocolate Doom)



161
162
163
164
165
166
167
168
169
170
171
# File 'lib/doom/render/renderer.rb', line 161

def draw_all_visplanes
  @visplanes.each do |plane|
    next unless plane.valid?

    if plane.texture == 'F_SKY1'
      draw_sky_plane(plane)
    else
      render_visplane_spans(plane)
    end
  end
end

#draw_floor_ceiling_backgroundObject



153
154
155
156
157
158
# File 'lib/doom/render/renderer.rb', line 153

def draw_floor_ceiling_background
  player_sector = @map.sector_at(@player_x, @player_y)
  return unless player_sector

  fill_uncovered_with_sector(player_sector)
end

#draw_sky_plane(plane) ⇒ Object

Render sky ceiling as columns (column-based like walls, not spans)



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/doom/render/renderer.rb', line 270

def draw_sky_plane(plane)
  sky_texture = @textures['SKY1']
  return unless sky_texture

  framebuffer = @framebuffer
  player_angle = @player_angle
  projection = @projection
  sky_width = sky_texture.width
  sky_height = sky_texture.height

  # Clamp to screen bounds
  minx = [plane.minx, 0].max
  maxx = [plane.maxx, SCREEN_WIDTH - 1].min

  (minx..maxx).each do |x|
    y1 = plane.top[x]
    y2 = plane.bottom[x]
    next if y1 > y2

    # Clamp y bounds
    y1 = 0 if y1 < 0
    y2 = SCREEN_HEIGHT - 1 if y2 >= SCREEN_HEIGHT

    # Sky X based on view angle (wraps around 256 degrees)
    column_angle = player_angle - Math.atan2(x - HALF_WIDTH, projection)
    sky_x = ((column_angle * 256 / Math::PI).to_i & 255) % sky_width
    column = sky_texture.column_pixels(sky_x)
    next unless column

    (y1..y2).each do |y|
      color = column[y % sky_height] || 0
      framebuffer[y * SCREEN_WIDTH + x] = color
    end
  end
end

#draw_span(plane, y, x1, x2) ⇒ Object

Render one horizontal span with texture mapping (R_MapPlane in Chocolate Doom)



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/doom/render/renderer.rb', line 223

def draw_span(plane, y, x1, x2)
  return if x1.nil? || x1 > x2 || y < 0 || y >= SCREEN_HEIGHT

  flat = @flats[plane.texture]
  return unless flat

  # Distance from horizon (y=100 for 200-high screen)
  dy = y - HALF_HEIGHT
  return if dy == 0

  # Plane height relative to player eye level
  plane_height = (plane.height - @player_z).abs
  return if plane_height == 0

  # Perpendicular distance to this row: distance = height * projection / dy
  perp_dist = plane_height * @projection / dy.abs

  # Calculate lighting for this distance
  light = calculate_flat_light(plane.light_level, perp_dist)
  cmap = @colormap.maps[light]

  # Cache locals for inner loop
  framebuffer = @framebuffer
  column_distscale = @column_distscale
  column_cos = @column_cos
  column_sin = @column_sin
  player_x = @player_x
  neg_player_y = -@player_y
  row_offset = y * SCREEN_WIDTH

  # Clamp to screen bounds
  x1 = 0 if x1 < 0
  x2 = SCREEN_WIDTH - 1 if x2 >= SCREEN_WIDTH

  # Draw each pixel in the span using while loop
  x = x1
  while x <= x2
    ray_dist = perp_dist * column_distscale[x]
    tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
    tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
    color = flat[tex_x, tex_y] || 0
    framebuffer[row_offset + x] = cmap[color]
    x += 1
  end
end

#fill_uncovered_with_sector(default_sector) ⇒ Object



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/doom/render/renderer.rb', line 377

def fill_uncovered_with_sector(default_sector)
  # Cache all instance variables as locals for faster access
  framebuffer = @framebuffer
  column_cos = @column_cos
  column_sin = @column_sin
  column_distscale = @column_distscale
  projection = @projection
  player_angle = @player_angle
  player_x = @player_x
  neg_player_y = -@player_y

  ceil_height = (default_sector.ceiling_height - @player_z).abs
  floor_height = (default_sector.floor_height - @player_z).abs
  ceil_flat = @flats[default_sector.ceiling_texture]
  floor_flat = @flats[default_sector.floor_texture]
  is_sky = default_sector.ceiling_texture == 'F_SKY1'
  sky_texture = is_sky ? @textures['SKY1'] : nil
  light_level = default_sector.light_level
  colormap_maps = @colormap.maps

  # Precompute y_slope for each row (perpendicular distance)
  y_slope_ceil = Array.new(HALF_HEIGHT + 1, 0.0)
  y_slope_floor = Array.new(HALF_HEIGHT + 1, 0.0)
  (1..HALF_HEIGHT).each do |dy|
    y_slope_ceil[dy] = ceil_height * projection / dy.to_f
    y_slope_floor[dy] = floor_height * projection / dy.to_f
  end

  # Draw ceiling (rows 0 to HALF_HEIGHT-1) using while loops for speed
  y = 0
  while y < HALF_HEIGHT
    dy = HALF_HEIGHT - y
    if dy > 0
      perp_dist = y_slope_ceil[dy]
      if perp_dist > 0
        light = calculate_flat_light(light_level, perp_dist)
        cmap = colormap_maps[light]
        row_offset = y * SCREEN_WIDTH

        if is_sky && sky_texture
          sky_height = sky_texture.height
          sky_width = sky_texture.width
          sky_y = y % sky_height
          x = 0
          while x < SCREEN_WIDTH
            column_angle = player_angle - Math.atan2(x - HALF_WIDTH, projection)
            sky_x = ((column_angle * 256 / Math::PI).to_i & 255) % sky_width
            color = sky_texture.column_pixels(sky_x)[sky_y] || 0
            framebuffer[row_offset + x] = color
            x += 1
          end
        elsif ceil_flat
          x = 0
          while x < SCREEN_WIDTH
            ray_dist = perp_dist * column_distscale[x]
            tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
            tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
            color = ceil_flat[tex_x, tex_y] || 0
            framebuffer[row_offset + x] = cmap[color]
            x += 1
          end
        end
      end
    end
    y += 1
  end

  # Draw floor (rows HALF_HEIGHT to SCREEN_HEIGHT-1)
  y = HALF_HEIGHT
  while y < SCREEN_HEIGHT
    dy = y - HALF_HEIGHT
    if dy > 0
      perp_dist = y_slope_floor[dy]
      if perp_dist > 0
        light = calculate_flat_light(light_level, perp_dist)
        cmap = colormap_maps[light]
        row_offset = y * SCREEN_WIDTH

        if floor_flat
          x = 0
          while x < SCREEN_WIDTH
            ray_dist = perp_dist * column_distscale[x]
            tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
            tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
            color = floor_flat[tex_x, tex_y] || 0
            framebuffer[row_offset + x] = cmap[color]
            x += 1
          end
        end
      end
    end
    y += 1
  end
end

#find_or_create_visplane(sector, height, texture, light_level, is_ceiling) ⇒ Object



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/doom/render/renderer.rb', line 306

def find_or_create_visplane(sector, height, texture, light_level, is_ceiling)
  # Find existing visplane with matching properties
  plane = @visplanes.find do |vp|
    vp.height == height &&
    vp.texture == texture &&
    vp.light_level == light_level &&
    vp.is_ceiling == is_ceiling
  end

  unless plane
    plane = Visplane.new(sector, height, texture, light_level, is_ceiling)
    @visplanes << plane
  end

  plane
end

#move_to(x, y) ⇒ Object



91
92
93
94
# File 'lib/doom/render/renderer.rb', line 91

def move_to(x, y)
  @player_x = x.to_f
  @player_y = y.to_f
end

#precompute_column_dataObject

Precompute column-based data for floor/ceiling rendering (R_InitLightTables-like)



139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/doom/render/renderer.rb', line 139

def precompute_column_data
  @column_cos ||= Array.new(SCREEN_WIDTH)
  @column_sin ||= Array.new(SCREEN_WIDTH)
  @column_distscale ||= Array.new(SCREEN_WIDTH)

  SCREEN_WIDTH.times do |x|
    dx = x - HALF_WIDTH
    column_angle = @player_angle - Math.atan2(dx, @projection)
    @column_cos[x] = Math.cos(column_angle)
    @column_sin[x] = Math.sin(column_angle)
    @column_distscale[x] = Math.sqrt(dx * dx + @projection * @projection) / @projection
  end
end

#render_frameObject



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/doom/render/renderer.rb', line 104

def render_frame
  clear_framebuffer
  reset_clipping

  @sin_angle = Math.sin(@player_angle)
  @cos_angle = Math.cos(@player_angle)

  # Precompute column angles for floor/ceiling rendering
  precompute_column_data

  # Draw floor/ceiling background first (will be partially overwritten by walls)
  draw_floor_ceiling_background

  # Initialize visplanes for tracking visible floor/ceiling spans
  @visplanes = []

  # Initialize drawsegs for sprite clipping
  @drawsegs = []

  # Render walls via BSP traversal
  render_bsp_node(@map.nodes.size - 1)

  # Draw visplanes for sectors different from background
  draw_all_visplanes

  # Save wall clip arrays for sprite clipping
  @sprite_ceiling_clip = @ceiling_clip.dup
  @sprite_floor_clip = @floor_clip.dup
  @sprite_wall_depth = @wall_depth.dup

  # Render sprites
  render_sprites if @sprites
end

#render_visplane_spans(plane) ⇒ Object

Render visplane using horizontal spans (R_MakeSpans in Chocolate Doom) This processes columns left-to-right, building spans and rendering them



175
176
177
178
179
180
181
182
183
184
185
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
# File 'lib/doom/render/renderer.rb', line 175

def render_visplane_spans(plane)
  return if plane.minx > plane.maxx

  spanstart = Array.new(SCREEN_HEIGHT)  # Track where each row's span started

  # Process columns left to right
  ((plane.minx)..(plane.maxx + 1)).each do |x|
    # Get current column bounds
    if x <= plane.maxx
      t2 = plane.top[x]
      b2 = plane.bottom[x]
      t2 = SCREEN_HEIGHT if t2 > b2  # Invalid = empty
    else
      t2, b2 = SCREEN_HEIGHT, -1  # Sentinel for final column
    end

    # Get previous column bounds
    if x > plane.minx
      t1 = plane.top[x - 1]
      b1 = plane.bottom[x - 1]
      t1 = SCREEN_HEIGHT if t1 > b1
    else
      t1, b1 = SCREEN_HEIGHT, -1
    end

    # Close spans that ended (visible in prev column, not in current)
    if t1 < SCREEN_HEIGHT
      # Rows visible in previous but not current (above current or below current)
      (t1..[b1, t2 - 1].min).each do |y|
        draw_span(plane, y, spanstart[y], x - 1) if spanstart[y]
        spanstart[y] = nil
      end
      ([t1, b2 + 1].max..b1).each do |y|
        draw_span(plane, y, spanstart[y], x - 1) if spanstart[y]
        spanstart[y] = nil
      end
    end

    # Open new spans (visible in current, not started yet)
    if t2 < SCREEN_HEIGHT
      (t2..b2).each do |y|
        spanstart[y] ||= x
      end
    end
  end
end

#set_player(x, y, z, angle) ⇒ Object



84
85
86
87
88
89
# File 'lib/doom/render/renderer.rb', line 84

def set_player(x, y, z, angle)
  @player_x = x.to_f
  @player_y = y.to_f
  @player_z = z.to_f
  @player_angle = angle * Math::PI / 180.0
end

#set_z(z) ⇒ Object



96
97
98
# File 'lib/doom/render/renderer.rb', line 96

def set_z(z)
  @player_z = z.to_f
end

#turn(degrees) ⇒ Object



100
101
102
# File 'lib/doom/render/renderer.rb', line 100

def turn(degrees)
  @player_angle += degrees * Math::PI / 180.0
end