Class: Doom::Render::Renderer

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

Direct Known Subclasses

HardwareRenderer, ZBufferRenderer

Defined Under Namespace

Classes: ProjectileStub

Constant Summary collapse

SKY_TEXTUREMID =

Render sky ceiling as columns (column-based like walls, not spans) Sky texture mid-point: texel row at screen center. Chocolate Doom: skytexturemid = SCREENHEIGHT/2 = 100 (for 200px screen). Scale factor: our 240px screen maps to DOOM's 200px sky coordinates.

100.0
SKY_YSCALE =

0.833 - maps our pixels to DOOM's 200px space

200.0 / SCREEN_HEIGHT
SKY_XSCALE =

Chocolate Doom: ANGLETOSKYSHIFT=22 gives 4 sky repetitions per 360 degrees. Full circle (2pi) * 512/pi = 1024 columns, masked to 256 = 4 repetitions.

512.0 / Math::PI

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of Renderer.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/doom/render/renderer.rb', line 59

def initialize(wad, map, textures, palette, colormap, flats, sprites = nil, animations = nil)
  @wad = wad
  @map = map
  @textures = textures
  @palette = palette
  @colormap = colormap
  @flats_array = flats
  @flats = flats.to_h { |f| [f.name, f] }
  # Opt-in: route the wall/floor/ceiling rasterisation through the
  # spinel_native-compiled port of this renderer's hot path. Everything
  # else (sprites, weapon, HUD, physics, sound) stays the real game.
  @native_kernel = !ENV["DOOM_NATIVE"].to_s.empty? && ENV["DOOM_NATIVE"] != "0"
  if @native_kernel
    warn "[doom] compiling spinel_native render kernel (first run may take a few seconds)..."
    @sn = build_native_renderer
    @native_kernel = !@sn.nil?
    warn "[doom] spinel_native renderer ready" if @sn
  end
  @sprites = sprites
  @animations = animations
  @hidden_things = nil
  @combat = nil
  @monster_ai = nil
  @players = []       # Every player in the world, for drawing the others
  @view_player = nil  # The one we are looking through, never drawn
  @leveltime = 0
  @skip_background_fill = false

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

  @player_x = 0.0
  @player_y = 0.0
  @player_z = 41.0
  @player_angle = 0.0
  @sin_angle = 0.0
  @cos_angle = 1.0

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

  # Precomputed column data (cached based on player angle)
  @column_cos = Array.new(SCREEN_WIDTH)
  @column_sin = Array.new(SCREEN_WIDTH)
  @cached_player_angle = nil

  # Column distance scale is constant (doesn't depend on player angle)
  @column_distscale = Array.new(SCREEN_WIDTH) do |x|
    dx = x - HALF_WIDTH
    Math.sqrt((dx * dx) + (@projection * @projection)) / @projection
  end

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

  # Per-sprite clip buffers, reset (not reallocated) for each draw_sprite call
  @sprite_clipbot = Array.new(SCREEN_WIDTH, -2)
  @sprite_cliptop = Array.new(SCREEN_WIDTH, -2)

  # 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)
  @sprite_wall_depth = Array.new(SCREEN_WIDTH, Float::INFINITY)

  # Preallocated y_slope arrays for floor/ceiling rendering (avoids per-frame allocation)
  @y_slope_ceil = Array.new(HALF_HEIGHT + 1, 0.0)
  @y_slope_floor = Array.new(HALF_HEIGHT + 1, 0.0)
end

Instance Attribute Details

#animationsObject (readonly)

Returns the value of attribute animations.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def animations
  @animations
end

#colormapObject (readonly)

Returns the value of attribute colormap.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def colormap
  @colormap
end

#combat=(value) ⇒ Object (writeonly)

Sets the attribute combat

Parameters:

  • the value to set the attribute combat to.



132
133
134
# File 'lib/doom/render/renderer.rb', line 132

def combat=(value)
  @combat = value
end

#cos_angleObject (readonly)

Returns the value of attribute cos_angle.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def cos_angle
  @cos_angle
end

#flatsObject (readonly)

Returns the value of attribute flats.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def flats
  @flats
end

#framebufferObject (readonly)

Returns the value of attribute framebuffer.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def framebuffer
  @framebuffer
end

#hidden_things=(value) ⇒ Object (writeonly)

Sets the attribute hidden_things

Parameters:

  • the value to set the attribute hidden_things to.



132
133
134
# File 'lib/doom/render/renderer.rb', line 132

def hidden_things=(value)
  @hidden_things = value
end

#leveltime=(value) ⇒ Object (writeonly)

Sets the attribute leveltime

Parameters:

  • the value to set the attribute leveltime to.



132
133
134
# File 'lib/doom/render/renderer.rb', line 132

def leveltime=(value)
  @leveltime = value
end

#mapObject (readonly)

Returns the value of attribute map.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def map
  @map
end

#monster_ai=(value) ⇒ Object (writeonly)

Sets the attribute monster_ai

Parameters:

  • the value to set the attribute monster_ai to.



132
133
134
# File 'lib/doom/render/renderer.rb', line 132

def monster_ai=(value)
  @monster_ai = value
end

#paletteObject (readonly)

Returns the value of attribute palette.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def palette
  @palette
end

#player_angleObject (readonly)

Returns the value of attribute player_angle.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def player_angle
  @player_angle
end

#player_xObject (readonly)

Returns the value of attribute player_x.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def player_x
  @player_x
end

#player_yObject (readonly)

Returns the value of attribute player_y.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def player_y
  @player_y
end

#player_zObject (readonly)

Returns the value of attribute player_z.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def player_z
  @player_z
end

#playersObject

Returns the value of attribute players.



133
134
135
# File 'lib/doom/render/renderer.rb', line 133

def players
  @players
end

#sin_angleObject (readonly)

Returns the value of attribute sin_angle.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def sin_angle
  @sin_angle
end

#skip_background_fillObject

Returns the value of attribute skip_background_fill.



133
134
135
# File 'lib/doom/render/renderer.rb', line 133

def skip_background_fill
  @skip_background_fill
end

#spritesObject (readonly)

Returns the value of attribute sprites.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def sprites
  @sprites
end

#texturesObject (readonly)

Returns the value of attribute textures.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def textures
  @textures
end

#view_playerObject

Returns the value of attribute view_player.



133
134
135
# File 'lib/doom/render/renderer.rb', line 133

def view_player
  @view_player
end

#wadObject (readonly)

Returns the value of attribute wad.



130
131
132
# File 'lib/doom/render/renderer.rb', line 130

def wad
  @wad
end

Instance Method Details

#apply_view(x, y, z, angle_radians) ⇒ Object

Point the camera at a pose. This is the renderer's real input now that Game::Player owns the player: game logic mutates the player, and the camera is aimed once per frame at an interpolated pose. Angle is in radians here, unlike the older degree-based setters below.



226
227
228
229
230
231
232
233
# File 'lib/doom/render/renderer.rb', line 226

def apply_view(x, y, z, angle_radians)
  @player_x = x
  @player_y = y
  @player_z = z
  @player_angle = angle_radians
  @sin_angle = Math.sin(angle_radians)
  @cos_angle = Math.cos(angle_radians)
end

#build_native_rendererObject



309
310
311
312
313
314
315
316
317
# File 'lib/doom/render/renderer.rb', line 309

def build_native_renderer
  # Dynamic form so the spinel AOT compiler ignores this require (it pulls
  # in the spinel_native gem, irrelevant to an AOT build); CRuby is unaffected.
  send(:require_relative, "spinel_native_renderer")
  SpinelNativeRenderer.new(@map, @palette, @colormap, @textures, @flats_array)
rescue StandardError, LoadError => e
  warn "[doom] spinel_native renderer unavailable, using Ruby renderer (#{e.message.lines.first.to_s.strip})"
  nil
end

#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



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/doom/render/renderer.rb', line 519

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

  unless 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

  # Update hash to point to the new plane (for subsequent lookups)
  key = [plane.height, plane.texture, plane.light_level, plane.is_ceiling]
  @visplane_hash[key] = new_plane

  new_plane
end

#draw_all_visplanesObject

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



343
344
345
346
347
348
349
350
351
352
353
# File 'lib/doom/render/renderer.rb', line 343

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



333
334
335
336
337
338
339
340
# File 'lib/doom/render/renderer.rb', line 333

def draw_floor_ceiling_background
  return if @skip_background_fill

  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



464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/doom/render/renderer.rb', line 464

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: 4 repetitions per 360 degrees (matching ANGLETOSKYSHIFT=22)
    column_angle = player_angle - Math.atan2(x - HALF_WIDTH, projection)
    sky_x = (column_angle * SKY_XSCALE).to_i % sky_width
    column = sky_texture.column_pixels(sky_x)
    next unless column

    # Sky Y: texel = skytexturemid + (y - centery) * scale
    # Maps texel 0 to screen top, texel 100 to horizon (1:1 for DOOM's 200px)
    (y1..y2).each do |y|
      tex_y = (SKY_TEXTUREMID + ((y - HALF_HEIGHT) * SKY_YSCALE)).to_i % sky_height
      color = column[tex_y]
      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)



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
# File 'lib/doom/render/renderer.rb', line 407

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

  flat = @flats[anim_flat(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
  flat_pixels = flat.pixels

  # 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_pixels[(tex_y * 64) + tex_x]
    framebuffer[row_offset + x] = cmap[color]
    x += 1
  end
end

#fill_uncovered_with_sector(default_sector) ⇒ Object



577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
# File 'lib/doom/render/renderer.rb', line 577

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[anim_flat(default_sector.ceiling_texture)]
  floor_flat = @flats[anim_flat(default_sector.floor_texture)]
  ceil_pixels = ceil_flat&.pixels
  floor_pixels = floor_flat&.pixels
  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

  # Compute y_slope for each row (perpendicular distance) - reuse preallocated arrays
  y_slope_ceil = @y_slope_ceil
  y_slope_floor = @y_slope_floor
  (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_w = sky_texture.width
          sky_h = sky_texture.height
          sky_y = (SKY_TEXTUREMID + ((y - HALF_HEIGHT) * SKY_YSCALE)).to_i % sky_h
          x = 0
          while x < SCREEN_WIDTH
            column_angle = player_angle - Math.atan2(x - HALF_WIDTH, projection)
            sky_x = (column_angle * SKY_XSCALE).to_i % sky_w
            color = sky_texture.column_pixels(sky_x)[sky_y]
            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_pixels[(tex_y * 64) + tex_x]
            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_pixels[(tex_y * 64) + tex_x]
            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



503
504
505
506
507
508
509
510
511
512
513
514
515
# File 'lib/doom/render/renderer.rb', line 503

def find_or_create_visplane(sector, height, texture, light_level, is_ceiling)
  # O(1) hash lookup instead of O(n) linear search
  key = [height, texture, light_level, is_ceiling]
  plane = @visplane_hash[key]

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

  plane
end

#native_wall_passObject

Wall/floor/ceiling/sky rasterisation through the spinel_native-compiled port of this renderer's hot path. Fills @framebuffer plus the clip rows the Ruby sprite pass reads. Falls back to the Ruby path if the kernel cannot be built (e.g. no spinel compiler available).



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/doom/render/renderer.rb', line 291

def native_wall_pass
  @sn ||= build_native_renderer
  unless @sn
    @native_kernel = false
    return render_frame
  end
  # The sprite pass (kept in Ruby) transforms things with @sin_angle /
  # @cos_angle, so they must be set for the current frame even though the
  # native kernel computes its own trig internally.
  @sin_angle = Math.sin(@player_angle)
  @cos_angle = Math.cos(@player_angle)
  fb = @sn.render_at(@player_x, @player_y, @player_z, @player_angle)
  @framebuffer.replace(fb)
  @ceiling_clip.replace(@sn.ceiling_clip)
  @floor_clip.replace(@sn.floor_clip)
  @wall_depth.replace(@sn.wall_depth)
end

#precompute_column_dataObject

Precompute column-based data for floor/ceiling rendering (R_InitLightTables-like) Cached: only recomputes sin/cos when player angle changes



321
322
323
324
325
326
327
328
329
330
331
# File 'lib/doom/render/renderer.rb', line 321

def precompute_column_data
  return if @cached_player_angle == @player_angle

  @cached_player_angle = @player_angle

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

#render_frameObject



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/doom/render/renderer.rb', line 244

def render_frame
  if @native_kernel
    native_wall_pass
    @drawsegs = []
  else
    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 player's sector floor/ceiling as background fallback.
    # Visplanes (with correct per-sector lighting) overwrite this for sectors
    # with different properties. This only remains visible at gaps between
    # same-property sectors where the light level matches anyway.
    draw_floor_ceiling_background

    # Initialize visplanes for tracking visible floor/ceiling spans
    @visplanes = []
    @visplane_hash = {} # Hash for O(1) lookup by (height, texture, light_level, is_ceiling)

    # 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
  end

  # Save wall clip arrays for sprite clipping (reuse preallocated arrays)
  @sprite_ceiling_clip.replace(@ceiling_clip)
  @sprite_floor_clip.replace(@floor_clip)
  @sprite_wall_depth.replace(@wall_depth)

  # R_DrawMasked: render sprites and masked middle textures interleaved
  draw_masked 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



357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
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
# File 'lib/doom/render/renderer.rb', line 357

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 = SCREEN_HEIGHT
      b2 = -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 = SCREEN_HEIGHT
      b1 = -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)
    next unless t2 < SCREEN_HEIGHT

    (t2..b2).each do |y|
      spanstart[y] ||= x
    end
  end
end

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



235
236
237
238
239
240
241
242
# File 'lib/doom/render/renderer.rb', line 235

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
  @sin_angle = Math.sin(@player_angle)
  @cos_angle = Math.cos(@player_angle)
end

#sprite_diagnosticsObject

Diagnostic: returns info about all sprites and why they are/aren't visible



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
170
171
172
173
174
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 136

def sprite_diagnostics
  return [] unless @sprites

  results = []
  @map.things.each do |thing|
    prefix = @sprites.prefix_for(thing.type)
    next unless prefix

    info = { type: thing.type, x: thing.x, y: thing.y, prefix: prefix }

    view_x, view_y = transform_point(thing.x, thing.y)
    info[:view_x] = view_x.round(1)
    info[:view_y] = view_y.round(1)

    if view_y <= 0
      info[:status] = 'behind_player'
      results << info
      next
    end

    dist = view_y
    screen_x = HALF_WIDTH + (view_x * @projection / view_y)
    info[:screen_x] = screen_x.round(1)
    info[:dist] = dist.round(1)

    dx = thing.x - @player_x
    dy = thing.y - @player_y
    angle_to_thing = Math.atan2(dy, dx)
    sprite = @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
    unless sprite
      info[:status] = 'no_sprite_frame'
      results << info
      next
    end

    sprite_scale = @projection / dist
    sprite_half_width = (sprite.width * @projection / dist / 2).to_i
    info[:sprite_scale] = sprite_scale.round(3)

    if screen_x + sprite_half_width < 0
      info[:status] = 'off_screen_left'
    elsif screen_x - sprite_half_width >= SCREEN_WIDTH
      info[:status] = 'off_screen_right'
    else
      # Check drawseg clipping
      sprite_left = (screen_x - (sprite.left_offset * sprite_scale)).to_i
      sprite_right = sprite_left + (sprite.width * sprite_scale).to_i - 1
      x1 = [sprite_left, 0].max
      x2 = [sprite_right, SCREEN_WIDTH - 1].min

      sector = @map.sector_at(thing.x, thing.y)
      thing_floor = sector ? sector.floor_height : 0
      thing_floor + sprite.top_offset

      clipping_segs = []
      @drawsegs.reverse_each do |ds|
        next if ds.x1 > x2 || ds.x2 < x1
        next if ds.silhouette == SIL_NONE

        lowscale = [ds.scale1, ds.scale2].min
        highscale = [ds.scale1, ds.scale2].max

        if highscale < sprite_scale
          next
        elsif lowscale < sprite_scale
          next unless point_on_seg_side(thing.x, thing.y, ds.curline)
        end

        clipping_segs << {
          x1: ds.x1, x2: ds.x2,
          scale: "#{ds.scale1.round(3)}..#{ds.scale2.round(3)}",
          sil: ds.silhouette
        }
      end

      info[:screen_range] = "#{x1}..#{x2}"
      info[:clipping_segs] = clipping_segs.size
      info[:clipping_detail] = clipping_segs
      info[:status] = 'visible'
    end

    results << info
  end
  results
end