Class: Quake::Renderer::GLLightmap

Inherits:
Object
  • Object
show all
Defined in:
lib/quake/renderer/gl_lightmap.rb

Overview

Computes and uploads lightmaps for BSP faces. Each face's lightmap is a small texture (typically 4x4 to 18x18) derived from the lighting lump. Lightmap UVs are computed from the face's texture-space extents, quantized to a 16-unit grid.

Defined Under Namespace

Classes: LightmapInfo

Constant Summary collapse

LIGHTMAP_BLOCK_WIDTH =
128
LIGHTMAP_BLOCK_HEIGHT =
128
MAX_LIGHTSTYLES =
64
DEFAULT_LIGHTSTYLES =
{
  0 => "m",
  1 => "mmnmmommommnonmmonqnmmo",
  2 => "abcdefghijklmnopqrstuvwxyzyxwvutsrqponmlkjihgfedcba",
  3 => "mmmmmaaaaammmmmaaaaaabcdefgabcdefg",
  4 => "mamamamamama",
  5 => "jklmnopqrstuvwxyzyxwvutsrqponmlkj",
  6 => "nmonqnmomnmomomno",
  7 => "mmmaaaabcdefgmmmmaaaammmaamm",
  8 => "mmmaaammmaaammmabcdefaaaammmmabcdefmmmaaaa",
  9 => "aaaaaaaazzzzzzzz",
  10 => "mmamammmmammamamaaamammma",
  11 => "abcdefghijklmnopqrrqponmlkjihgfedcba",
  63 => "a"
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(level, palette, lightstyles: nil) ⇒ GLLightmap

Returns a new instance of GLLightmap.



35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/quake/renderer/gl_lightmap.rb', line 35

def initialize(level, palette, lightstyles: nil)
  @level = level
  @palette = palette
  @face_lightmaps = {}
  @block_textures = []    # GL texture ids for each block
  @block_allocated = []   # row allocation per block column
  @block_pixels = []      # RGBA pixel data per block
  @face_lightmap_faces = {}
  @uploaded = false
  @lightstyles = self.class.default_lightstyles
  Array(lightstyles).each_with_index { |map, style| @lightstyles[style] = map if map }
  animate_lightstyles(0.0)
end

Instance Attribute Details

#face_lightmapsObject (readonly)

face_index -> LightmapInfo



33
34
35
# File 'lib/quake/renderer/gl_lightmap.rb', line 33

def face_lightmaps
  @face_lightmaps
end

#lightstyle_valuesObject (readonly)

face_index -> LightmapInfo



33
34
35
# File 'lib/quake/renderer/gl_lightmap.rb', line 33

def lightstyle_values
  @lightstyle_values
end

Class Method Details

.default_lightstylesObject



49
50
51
# File 'lib/quake/renderer/gl_lightmap.rb', line 49

def self.default_lightstyles
  Array.new(MAX_LIGHTSTYLES) { |style| DEFAULT_LIGHTSTYLES[style] }
end

.lightstyle_values_for(time, lightstyles) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
# File 'lib/quake/renderer/gl_lightmap.rb', line 53

def self.lightstyle_values_for(time, lightstyles)
  frame = (time.to_f * 10.0).to_i
  values = Array.new(MAX_LIGHTSTYLES, 256)
  MAX_LIGHTSTYLES.times do |style|
    map = lightstyles[style]
    next if map.nil? || map.empty?

    values[style] = (map.getbyte(frame % map.length) - "a".ord) * 22
  end
  values
end

Instance Method Details

#animate_lightstyles(time) ⇒ Object



75
76
77
78
# File 'lib/quake/renderer/gl_lightmap.rb', line 75

def animate_lightstyles(time)
  @lightstyle_values = self.class.lightstyle_values_for(time, @lightstyles)
  @lightstyle_values
end

#bind(face_index) ⇒ Object



132
133
134
135
136
# File 'lib/quake/renderer/gl_lightmap.rb', line 132

def bind(face_index)
  info = @face_lightmaps[face_index]
  return unless info
  GL.BindTexture(GL::TEXTURE_2D, info.gl_texture)
end

#build_allObject



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/quake/renderer/gl_lightmap.rb', line 111

def build_all
  animate_lightstyles(0.0) unless @lightstyle_values
  allocate_block

  @level.faces.each_with_index do |face, face_index|
    texinfo = @level.texinfo[face.texinfo_index]
    next if texinfo.nil?
    next if texinfo.flags & 1 != 0 # TEX_SPECIAL (sky/turb)
    # Faces with no samples (light_offset < 0) still get a lightmap: all
    # zeros renders them black, matching R_BuildLightMap. Only a missing
    # lighting lump means fullbright.
    next if @level.lighting.nil? || @level.lighting.empty?

    build_face_lightmap(face, face_index)
  end

  upload_blocks
  count = @face_lightmaps.size
  puts "Built #{count} lightmaps in #{@block_textures.size} blocks"
end

#lightmap_texcoords(face_index, vertex, texinfo) ⇒ Object

Compute lightmap UVs for a vertex given the face's lightmap info.



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
# File 'lib/quake/renderer/gl_lightmap.rb', line 139

def lightmap_texcoords(face_index, vertex, texinfo)
  info = @face_lightmaps[face_index]
  return [0.0, 0.0] unless info

  # Compute texture-space coordinate
  s = vertex.dot(texinfo.s_vec) + texinfo.s_offset
  t = vertex.dot(texinfo.t_vec) + texinfo.t_offset

  # Convert to lightmap UV: subtract texture mins, scale by 1/16.
  # The half-luxel centering is applied once, as the +0.5 texel below
  # (C adds +8 texture units before the /16 -- same thing).
  extents = face_extents(face_index)
  return [0.0, 0.0] unless extents

  tex_mins_s, tex_mins_t, _, _ = extents

  ls = (s - tex_mins_s) / (info.width * 16.0)
  lt = (t - tex_mins_t) / (info.height * 16.0)

  # Map into atlas block coordinates
  atlas_s = (info.s_offset + ls * info.width + 0.5) / LIGHTMAP_BLOCK_WIDTH
  atlas_t = (info.t_offset + lt * info.height + 0.5) / LIGHTMAP_BLOCK_HEIGHT

  [atlas_s, atlas_t]
end

#lightstyles=(lightstyles) ⇒ Object



65
66
67
# File 'lib/quake/renderer/gl_lightmap.rb', line 65

def lightstyles=(lightstyles)
  Array(lightstyles).each_with_index { |map, style| @lightstyles[style] = map if style < MAX_LIGHTSTYLES }
end

#set_lightstyle(style, map) ⇒ Object



69
70
71
72
73
# File 'lib/quake/renderer/gl_lightmap.rb', line 69

def set_lightstyle(style, map)
  return if style.negative? || style >= MAX_LIGHTSTYLES

  @lightstyles[style] = map
end

#update(time, lightstyles: nil, dynamic_lights: nil) ⇒ Object



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
# File 'lib/quake/renderer/gl_lightmap.rb', line 80

def update(time, lightstyles: nil, dynamic_lights: nil)
  self.lightstyles = lightstyles if lightstyles
  old_values = @lightstyle_values || Array.new(MAX_LIGHTSTYLES, 256)
  new_values = animate_lightstyles(time)
  changed_styles = (0...MAX_LIGHTSTYLES).select { |style| old_values[style] != new_values[style] }

  # R_MarkLights: faces touched by dynamic lights this frame, plus faces
  # lit last frame (which must be re-baked back to their static light).
  dlit_faces = mark_dynamic_faces(Array(dynamic_lights))
  previously_dlit = @dlit_faces || {}
  @dlit_faces = dlit_faces
  return false if changed_styles.empty? && dlit_faces.empty? && previously_dlit.empty?

  changed_blocks = {}
  @face_lightmap_faces.each do |face_index, face|
    lights = dlit_faces[face_index]
    needs_rebake = lights ||
                   previously_dlit.key?(face_index) ||
                   (!changed_styles.empty? && face_uses_styles?(face, changed_styles))
    next unless needs_rebake

    info = @face_lightmaps[face_index]
    next unless info

    write_face_lightmap(face, info, face_index: face_index, dlights: lights)
    changed_blocks[info.block_index] = true
  end
  upload_changed_blocks(changed_blocks.keys) if @uploaded
  !changed_blocks.empty?
end