Class: Zif::RenderTarget

Inherits:
Object
  • Object
show all
Includes:
Serializable
Defined in:
app/lib/zif/render_target.rb

Overview

For creating and updating Render Targets.

A render target in DRGTK is a way to programmatically create a static image out of sprites. It acts just like $gtk.args.outputs in that it accepts an array of sprites and other primitives. It gets rendered into memory at the end of the tick where it is referenced out of $gtk.args.outputs, based on its contents. To display the result, you need to send $gtk.args.outputs a sprite which references the name of the render target as its path.

This class holds references to the #sprites and all of the configuration options necessary to invoke this concept in DragonRuby GTK. It also includes a Sprite referencing the created image in #containing_sprite.

Once set up, you can force DRGTK to fully render the image using #redraw, which redraws everything.

Although an already rendered RenderTarget is cheap to display, one drawback of using RenderTargets is that they take a little longer to process in the first place, compared to simply drawing sprites the normal way. This can become an issue if you need to frequently update the RenderTarget due to changes on the source sprites. Say you have a large tile map you pregenerate as a RenderTarget when the game loads. If you need to change a single tile, like if that tile represents a door and the door opens, normally you would need to regenerate the entire RenderTarget using all of the source sprites.

A technique the DragonRuby community (specifically Islacrusez, oeloeloel) has identified to overcome this performance issue is to build another RenderTarget using the previously rendered one, plus whatever sprites are changing. See this example implementation of this technique: github.com/oeloeloel/persistent-outputs

This strategy is implemented here by #redraw_from_buffer. Since this is inherently an additive process, #redraw_from_buffer allows you to cut out a single rectangle (via the cut_rect param) from the old image to handle deletions.

See DRGTK docs on Render Targets docs.dragonruby.org/#—-advanced-rendering—simple-render-targets—main.rb

Examples:

Setting up a basic paint canvas

paint_canvas = Zif::RenderTarget.new(:my_paint_canvas, bg_color: :white, width: 1000, height: 500)
paint_canvas.sprites << @all_current_brushstrokes
paint_canvas.redraw
$gtk.args.outputs.static_sprites << paint_canvas.containing_sprite

# Some time later, you can add new brush strokes and delete a rectangle:

minimap = # ... a different sprite referencing the RenderTarget as path
new_brushstroke = # ... a new Sprite to add to the render
erase_rect = [200, 200, 10, 10] # Let's say you erased something, too
paint_canvas.redraw_from_buffer([new_brushstroke], erase_rect, [minimap])

Direct Known Subclasses

Layers::SimpleLayer

Instance Attribute Summary collapse

1. Public Interface collapse

2. Private-ish methods collapse

Methods included from Serializable

#inspect, #serialize, #to_s

Constructor Details

#initialize(name, bg_color: :black, width: 1, height: 1, z_index: 0) ⇒ RenderTarget

Returns a new instance of RenderTarget.

Parameters:

  • name (Symbol, String)
  • bg_color (Symbol, Array<Integer>) (defaults to: :black)
  • width (Integer) (defaults to: 1)
  • height (Integer) (defaults to: 1)
  • z_index (Integer) (defaults to: 0)

82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'app/lib/zif/render_target.rb', line 82

def initialize(name, bg_color: :black, width: 1, height: 1, z_index: 0)
  @name       = name
  @width      = width
  @height     = height
  @z_index    = z_index
  @sprites    = []
  @labels     = []
  @primitives = []
  # This could probably be improved with a Color module
  @bg_color = case bg_color
              when :black
                [0, 0, 0, 0]
              when :white
                [255, 255, 255, 0]
              else
                bg_color
              end
end

Instance Attribute Details

#bg_colorSymbol, Array<Integer>

Returns Accepts :black, :white, or an RGBA color array like [255,255,255,255].

Returns:

  • (Symbol, Array<Integer>)

    Accepts :black, :white, or an RGBA color array like [255,255,255,255]


60
61
62
# File 'app/lib/zif/render_target.rb', line 60

def bg_color
  @bg_color
end

#heightInteger

Returns The total height of the rendered image. This could be larger or smaller than the size used to display the image on the #containing_sprite.

Returns:

  • (Integer)

    The total height of the rendered image. This could be larger or smaller than the size used to display the image on the #containing_sprite.


57
58
59
# File 'app/lib/zif/render_target.rb', line 57

def height
  @height
end

#labelsArray<Zif::UI::Label>

Returns The list of labels this RenderTarget is rendering.

Returns:

  • (Array<Zif::UI::Label>)

    The list of labels this RenderTarget is rendering.


66
67
68
# File 'app/lib/zif/render_target.rb', line 66

def labels
  @labels
end

#nameString, Symbol

Returns The name of the render target. This is used when invoking the render target in DRGTK, so this must be unique.

Returns:

  • (String, Symbol)

    The name of the render target. This is used when invoking the render target in DRGTK, so this must be unique.


49
50
51
# File 'app/lib/zif/render_target.rb', line 49

def name
  @name
end

#primitivesArray<Zif::Sprite, Hash>

Returns The list of other primitives this RenderTarget is rendering.

Returns:

  • (Array<Zif::Sprite, Hash>)

    The list of other primitives this RenderTarget is rendering.


69
70
71
# File 'app/lib/zif/render_target.rb', line 69

def primitives
  @primitives
end

#spritesArray<Zif::Sprite>

Returns The list of sprites this RenderTarget is rendering.

Returns:

  • (Array<Zif::Sprite>)

    The list of sprites this RenderTarget is rendering.


63
64
65
# File 'app/lib/zif/render_target.rb', line 63

def sprites
  @sprites
end

#widthInteger

Returns The total width of the rendered image. This could be larger or smaller than the size used to display the image on the #containing_sprite.

Returns:

  • (Integer)

    The total width of the rendered image. This could be larger or smaller than the size used to display the image on the #containing_sprite.


53
54
55
# File 'app/lib/zif/render_target.rb', line 53

def width
  @width
end

#z_indexInteger

Returns When comparing the containing sprite with other sprites, this sets the layering order.

Returns:

  • (Integer)

    When comparing the containing sprite with other sprites, this sets the layering order.


72
73
74
# File 'app/lib/zif/render_target.rb', line 72

def z_index
  @z_index
end

Instance Method Details

#clicked?(point, kind = :up) ⇒ Object?

This implements functionality for the Services::InputService to check for click handlers amongst the list of #sprites and #primitives assigned to this object. It will adjust the point from the screen itself to the relative position of the #sprites within the #containing_sprite, and send this adjusted point to the #sprites #clicked? method.

Parameters:

  • point (Array<Integer>)
    x, y

    position Array of the current mouse click.

  • kind (Symbol) (defaults to: :up)

    The kind of click coming through, one of [:up, :down, :changed]

Returns:

  • (Object, nil)

    If any #sprites or #primitives respond positively to being #clicked? at the adjusted point, this returns the object which was clicked. Otherwise return nil.

See Also:


163
164
165
166
167
168
169
# File 'app/lib/zif/render_target.rb', line 163

def clicked?(point, kind=:up)
  relative = relative_point(point)
  # puts "#{self.class.name}:#{name}: clicked? #{point} -> relative #{relative}"

  find_and_return = ->(sprite) { sprite.respond_to?(:clicked?) && sprite.clicked?(relative, kind) }
  @sprites.reverse_each.find(&find_and_return) || @primitives.reverse_each.find(&find_and_return)
end

#containing_spriteZif::Sprite

Returns A sprite which references this render target as its path, and set to the same width and height by default.

Returns:

  • (Zif::Sprite)

    A sprite which references this render target as its path, and set to the same width and height by default


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'app/lib/zif/render_target.rb', line 103

def containing_sprite
  return @containing_sprite if @containing_sprite

  redraw
  @containing_sprite = Zif::Sprite.new.tap do |s|
    s.name = "rt_#{@name}_containing_sprite"
    s.x = 0
    s.y = 0
    s.z_index = @z_index
    s.w = @width
    s.h = @height
    s.path = @name
    s.source_x = 0
    s.source_y = 0
    s.source_w = @width
    s.source_h = @height
    s.render_target = self
  end
end

#cut_containing_sprites(rect) ⇒ Array<Hash<Symbol, Numeric>>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

.

                right
                  v
@height -> +------+---+
           |   2  |   |
           |      | 3 |
    top -> +----+-+   |
           |    |r|   |
           | 1  +-+---+ <- bottom
           |    |  4  |
           |    |     |
    0,0 -> +----+-----+
                ^     ^
               left   @width

This creates four sprite-hashes to capture the area outside of a rectangle, used in #redraw_from_buffer

Parameters:

  • rect (Array<Integer>)

    [x, y, w, h]

Returns:

  • (Array<Hash<Symbol, Numeric>>)

    four sprites around the rect


295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
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
# File 'app/lib/zif/render_target.rb', line 295

def cut_containing_sprites(rect)
  left   = rect[0]
  bottom = rect[1]
  right  = left + rect[2]
  top    = bottom + rect[3]

  [
    # 1
    {
      x:        0,
      source_x: 0,
      y:        0,
      source_y: 0,
      w:        left,
      source_w: left,
      h:        top,
      source_h: top,
      path:     @name
    },
    # 2
    {
      x:        0,
      source_x: 0,
      y:        top,
      source_y: top,
      w:        right,
      source_w: right,
      h:        @height - top,
      source_h: @height - top,
      path:     @name
    },
    # 3
    {
      x:        right,
      source_x: right,
      y:        bottom,
      source_y: bottom,
      w:        @width - right,
      source_w: @width - right,
      h:        @height - bottom,
      source_h: @height - bottom,
      path:     @name
    },
    # 4
    {
      x:        left,
      source_x: left,
      y:        0,
      source_y: 0,
      w:        @width - left,
      source_w: @width - left,
      h:        bottom,
      source_h: bottom,
      path:     @name
    }
  ]
end

#exclude_from_serializeObject


204
205
206
# File 'app/lib/zif/render_target.rb', line 204

def exclude_from_serialize
  %w[sprites primitives]
end

#full_containing_spriteObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Like #containing_sprite, returns a Zif::Sprite referencing #name as path at the full width. This is private because it's used internally in #redraw_from_buffer, it should not be modified externally


356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'app/lib/zif/render_target.rb', line 356

def full_containing_sprite
  return @full_containing_sprite if @full_containing_sprite

  @full_containing_sprite = Zif::Sprite.new.tap do |s|
    s.name = "rt_#{@name}_full_containing_sprite"
    s.x = 0
    s.y = 0
    s.z_index = @z_index
    s.w = @width
    s.h = @height
    s.path = @name
    s.source_x = 0
    s.source_y = 0
    s.source_w = @width
    s.source_h = @height
    s.render_target = self
  end
end

#project_from(rect = {}) ⇒ Object

Reassigns the source_x, source_y, source_w and source_h of the #containing_sprite.

Parameters:

  • rect (Hash<Symbol, Numeric>) (defaults to: {})

    A Hash containing x, y, w, h keys and Numeric values.


195
196
197
198
199
200
201
# File 'app/lib/zif/render_target.rb', line 195

def project_from(rect={})
  containing_sprite.assign(
    containing_sprite.source_rect_hash.merge(
      Zif::Sprite.rect_hash_to_source_hash(rect)
    )
  )
end

#project_to(rect = {}) ⇒ Object

Reassigns the x, y, width and height of the #containing_sprite.

Parameters:

  • rect (Hash<Symbol, Numeric>) (defaults to: {})

    A Hash containing x, y, w, h keys and Numeric values.


187
188
189
190
191
# File 'app/lib/zif/render_target.rb', line 187

def project_to(rect={})
  containing_sprite.assign(
    containing_sprite.rect_hash.merge(rect)
  )
end

#redrawObject

Call this method when you want to actually draw all #sprites onto the rendered image. You will need to redraw if you want any changes made to #sprites to be reflected in the #containing_sprite. This works by asking DRGTK for a reference to the render target by name, and resetting the #width and #height. DRGTK detects this and performs the render at the end of the tick. Therefore, please do not attempt to reference this render target directly via $gtk.args outside of this class - it will cause the render target to be redrawn without directing it to draw the sprites contained here.


136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'app/lib/zif/render_target.rb', line 136

def redraw
  # puts "RenderTarget#redraw: #{@name} #{@width} #{@height} #{@sprites.length} sprites, #{@labels.length} labels"
  # $services&.named(:tracer)&..mark("#redraw: #{@name} Begin")
  targ = $gtk.args.outputs[@name]
  targ.width  = @width
  targ.height = @height

  # It's important to set the background color intentionally.  Even if alpha == 0, semi-transparent images in
  # render targets will pick up this color as an additive.  Usually you want black.
  targ.background_color = @bg_color
  targ.primitives << @primitives if @primitives&.any?
  targ.sprites    << @sprites    if @sprites&.any?
  targ.labels     << @labels     if @labels&.any?
  # $services&.named(:tracer)&..mark("#redraw: #{@name} Complete")
end

#redraw_from_buffer(sprites: [], cut_rect: nil, additional_containing_sprites: []) ⇒ Object

TODO:

Need some examples of double buffering

Double Buffering

This method is for drawing new sprites on top of an already rendered RT, and optionally cut out a rectangle from the RT (for deletion) You might want to use this if your list of #sprites for the RT is very large, so rendering all of the sprites is slow, and you just need to modify a small subset.

The classic example is a paint application, where each tick adds a new brush stroke to the canvas. Instead of letting the #sprites array grow unbounded and redrawing all of them for each stroke, simply take the previous canvas and draw a new sprite on top of it.

A more complex example is if a single tile is changing inside of a large tile map. Instead of redrawing every tile, delete the tile which is changing from the already rendered target, and then redraw just the changing tile.

One drawback of using this is that you are forced to specify a single rectangle area which is changing.

Another drawback to using this is that the path for any sprite referencing the RenderTarget needs to change every time the buffer is switched. Since this references it's own containing sprite, we can update the path for that sprite automatically, but if there exist any additional sprites referencing the path (like if you have a map and a minimap pointing to the same path), those will have to be updated manually. To support this, you can pass additional containing sprites as an array to have their path updated.

See the ExampleApp::DoubleBufferRenderTest scene for a working example.

Parameters:

  • sprites (Array<Zif::Sprite>) (defaults to: [])

    Optional. An array of sprites to add to the image in this redraw.

  • cut_rect (Array<Integer>) (defaults to: nil)

    Optional. [x, y, w, h], the area to cut out from the existing buffer.

  • additional_containing_sprites (Array<Zif::Sprite>) (defaults to: [])

    Optional. An array of other containing sprites besides #containing_sprite whose path needs updating after switching buffers.


239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'app/lib/zif/render_target.rb', line 239

def redraw_from_buffer(sprites: [], cut_rect: nil, additional_containing_sprites: [])
  # $services&.named(:tracer)&..mark("RenderTarget#redraw_from_buffer: #{@name} Begin")
  source_buffer_sprites = cut_rect ? cut_containing_sprites(cut_rect) : [full_containing_sprite]

  set_inactive_buffer_name unless @inactive_buffer_name
  # puts "RenderTarget#redraw_from_buffer: name: #{@name}, inactive: #{@inactive_buffer_name}"
  # puts "RenderTarget#redraw_from_buffer: cut_rect: #{cut_rect}, source: #{source_buffer_sprites.inspect}"

  # $services&.named(:tracer)&..mark("RenderTarget#redraw_from_buffer: #{@name} Sprites")
  targ = $gtk.args.outputs[@inactive_buffer_name]
  targ.width  = @width
  targ.height = @height
  # $services&.named(:tracer)&..mark("RenderTarget#redraw_from_buffer: #{@name} HW")
  targ.background_color = @bg_color
  targ.sprites << [source_buffer_sprites] + sprites

  switch_buffer(additional_containing_sprites)
  # $services&.named(:tracer)&..mark("RenderTarget#redraw_from_buffer: #{@name} End")
end

#relative_point(point) ⇒ Array<Integer>

Convert the positional [x, y] array point from the screen coordinates, to the relative coordinates inside the #containing_sprite

Parameters:

  • point (Array<Integer>)
    x, y

    position Array in the context of the screen

Returns:

  • (Array<Integer>)

    point [x, y] position Array in the context of the #containing_sprite


175
176
177
178
179
180
181
182
183
# File 'app/lib/zif/render_target.rb', line 175

def relative_point(point)
  Zif.add_positions(
    Zif.sub_positions(
      point, # FIXME??: Zif.position_math(:mult, point, containing_sprite.source_wh),
      containing_sprite.xy
    ),
    containing_sprite.source_xy
  )
end

#resize(w, h) ⇒ Object

Parameters:

  • w (Integer)

    Change width

  • h (Integer)

    Change height


125
126
127
128
# File 'app/lib/zif/render_target.rb', line 125

def resize(w, h)
  @width = w
  @height = h
end

#set_inactive_buffer_nameObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.


263
264
265
# File 'app/lib/zif/render_target.rb', line 263

def set_inactive_buffer_name
  @inactive_buffer_name = "#{@name}_buf"
end

#switch_buffer(additional_containing_sprites = []) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Switch the #containing_sprite (and optional additional containing sprites) path to the inactive buffer Swap names so the name is the inactive name and vice versa. Private, you should probably not call this directly, it is called automatically by #redraw_from_buffer

Parameters:

  • additional_containing_sprites (Array<Zif::Sprite>) (defaults to: [])

    An array of other containing sprites to update.


272
273
274
275
# File 'app/lib/zif/render_target.rb', line 272

def switch_buffer(additional_containing_sprites=[])
  ([containing_sprite] + additional_containing_sprites).each { |cs| cs.path = @inactive_buffer_name }
  @inactive_buffer_name, @name = @name, @inactive_buffer_name
end