dragonruby-rendering

Advanced DragonRuby GTK rendering — render targets, cameras with world/screen space, pixel arrays, HD/lowrez resolution, thick lines, blendmodes, viewport culling, tiling textures. Use when the user asks about advanced visual effects, camera systems, or rendering performance.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "dragonruby-rendering" with this command: npx skills add nitemaeric/dragonruby-skills/nitemaeric-dragonruby-skills-dragonruby-rendering

This skill covers advanced rendering patterns in DragonRuby GTK. For basic sprite/label/solid output see the main dragonruby skill.

Render Targets (Off-screen Buffers)

Render targets let you draw into a named virtual canvas, then use it as a sprite. They are very powerful for caching, effects, and UI composition.

# Draw into target each tick (transient! clears it each frame):
args.outputs[:hud].transient!
args.outputs[:hud].width  = 400
args.outputs[:hud].height = 200
args.outputs[:hud].background_color = [0, 0, 0, 0]  # transparent bg
args.outputs[:hud].sprites << { x: 0, y: 0, w: 32, h: 32, path: 'sprites/icon.png' }
args.outputs[:hud].labels  << { x: 200, y: 100, text: "HP: 3", alignment_enum: 1 }

# Composite into the main scene:
args.outputs.sprites << { x: 0, y: 520, w: 400, h: 200, path: :hud }

Cached render target (draw once, reuse)

# Build the target only on tick 0 (or when content changes):
if Kernel.tick_count == 0
  args.outputs[:map_tiles].width  = 2560
  args.outputs[:map_tiles].height = 1440
  world_tiles.each { |t| args.outputs[:map_tiles].sprites << t }
end

# Render a viewport portion of the cached target each tick:
args.outputs.sprites << {
  x: 0, y: 0, w: 1280, h: 720,
  path: :map_tiles,
  source_x: cam.x, source_y: cam.y,
  source_w: 1280, source_h: 720
}

Checking target readiness

return if args.outputs.render_targets.queued?(:my_target)
return unless args.outputs.render_targets.ready?(:my_target)

Blendmodes & Clipping

# Blendmode 0 = ignore alpha (overwrite). Use for hollow borders:
args.outputs[:border_target].primitives << [
  { x: 0, y: 0, w: w, h: h, path: :solid, r: 255, g: 255, b: 255 },
  { x: t, y: t, w: w - t*2, h: h - t*2, path: :empty, blendmode: 0 }
]

Clipping (viewport mask)

args.outputs[:clipped].width  = clip_w
args.outputs[:clipped].height = clip_h
args.outputs[:clipped].sprites << all_world_sprites

args.outputs.sprites << {
  x: clip_x, y: clip_y, w: clip_w, h: clip_h,
  path: :clipped,
  source_x: clip_x, source_y: clip_y,
  source_w: clip_w, source_h: clip_h
}

Camera Systems

Simple follow camera

# Camera state
args.state.camera ||= { x: 0, y: 0, scale: 1.0 }

# Smooth follow
args.state.camera.x = args.state.camera.x.lerp(player.x - 640, 0.08)
args.state.camera.y = args.state.camera.y.lerp(player.y - 360, 0.08)

# Apply camera to all world entities:
def world_to_screen(camera, rect)
  { **rect,
    x: rect.x - camera.x,
    y: rect.y - camera.y }
end

Scalable camera (zoom + pan)

VIEWPORT_W = 1280
VIEWPORT_H = 720

def to_screen_space(camera, rect)
  {
    **rect,
    x: rect[:x] * camera[:scale] - camera[:x] * camera[:scale] + VIEWPORT_W / 2,
    y: rect[:y] * camera[:scale] - camera[:y] * camera[:scale] + VIEWPORT_H / 2,
    w: rect[:w] * camera[:scale],
    h: rect[:h] * camera[:scale]
  }
end

def to_world_space(camera, rect)
  {
    x: (rect[:x] - VIEWPORT_W / 2 + camera[:x] * camera[:scale]) / camera[:scale],
    y: (rect[:y] - VIEWPORT_H / 2 + camera[:y] * camera[:scale]) / camera[:scale]
  }
end

# Usage:
args.outputs.sprites << world_entities.map { |e| to_screen_space(args.state.camera, e) }

Viewport culling (only render visible entities)

viewport = {
  x: camera.x - 640 / camera.scale,
  y: camera.y - 360 / camera.scale,
  w: 1280 / camera.scale,
  h: 720 / camera.scale
}

visible = Geometry.find_all_intersect_rect(viewport, args.state.world_entities)
args.outputs.sprites << visible.map { |e| to_screen_space(camera, e) }

Camera shake (trauma system)

args.state.trauma ||= 0.0
args.state.trauma = (args.state.trauma + 0.3).clamp(0, 1)  # on hit

# Apply shake:
shake = 20.0 * args.state.trauma ** 2
args.state.camera.x_offset = shake.randomize(:ratio, :sign)
args.state.camera.y_offset = shake.randomize(:ratio, :sign)
args.state.trauma *= 0.93   # decay

Z-targeting / orbit camera

args.state.camera.angle ||= 0
distance = 200
target_x = entity.x + args.state.camera.angle.vector_x * distance
target_y = entity.y + args.state.camera.angle.vector_y * distance
args.state.camera.angle += 1   # orbit

Render target + camera (full pipeline)

# Draw world to a render target using camera offset:
args.outputs[:scene].transient!
args.outputs[:scene].width  = 1280
args.outputs[:scene].height = 720
args.outputs[:scene].sprites << visible_tiles.map { |t|
  t.merge(x: t.x - cam.x, y: t.y - cam.y)
}

# Blit scene + apply shake:
args.outputs.sprites << {
  x: cam.x_offset.to_i, y: cam.y_offset.to_i,
  w: 1280, h: 720, path: :scene
}

Pixel Arrays

Direct pixel-level manipulation for dynamic textures:

# Create a pixel array (ABGR 32-bit integers):
w = 64; h = 64
args.pixel_array(:canvas).width  = w
args.pixel_array(:canvas).height = h

# Fill solid black:
args.pixel_array(:canvas).pixels.fill(0xFF000000, 0, w * h)

# Draw a horizontal green line at row y:
y = 32
args.pixel_array(:canvas).pixels.fill(0xFF00FF00, y * w, w)

# Set individual pixel (x, y):
def set_pixel(px_array, w, x, y, abgr)
  px_array.pixels[y * w + x] = abgr
end

# ABGR hex format: 0xAABBGGRR
RED   = 0xFF0000FF
GREEN = 0xFF00FF00
BLUE  = 0xFFFF0000

# Animate pixel-by-pixel scanner:
args.state.scan_pos = (args.state.scan_pos || 0 + 1) % h
args.pixel_array(:scanner).width  = w
args.pixel_array(:scanner).height = h
args.pixel_array(:scanner).pixels.fill(0xFF000000, 0, w * h)
args.pixel_array(:scanner).pixels.fill(0xFF00FF00, args.state.scan_pos * w, w)

# Use pixel array as a sprite:
args.outputs.sprites << { x: 100, y: 100, w: w, h: h, path: :canvas }

Load pixels from a file

px = GTK.get_pixels('sprites/map.png')  # => { w:, h:, pixels: [] }
args.pixel_array(:map).width  = px.w
args.pixel_array(:map).height = px.h
args.pixel_array(:map).pixels = px.pixels

HD & High-DPI Rendering

Automatic HD sprite selection

DragonRuby automatically picks the right resolution file by naming convention:

  • 720p: player.png
  • 1080p: player@125.png
  • 1440p: player@200.png
  • 4K: player@300.png

No code changes needed — just provide the additional files.

# Query current rendering scale at runtime:
args.grid.texture_scale_enum  # integer scale enum
args.grid.native_scale        # float scale multiplier

Allscreen properties (ultrawide/notch support)

args.grid.allscreen_w         # full renderable width (beyond safe area)
args.grid.allscreen_h
args.grid.allscreen_offset_x  # offset from left to safe area
args.grid.left                # safe area left edge
args.grid.right               # safe area right edge

# Background that fills to screen edges:
args.outputs.sprites << {
  x: args.grid.allscreen_left,
  y: args.grid.allscreen_bottom,
  w: args.grid.allscreen_w,
  h: args.grid.allscreen_h,
  path: 'sprites/bg.png',
  source_x: 2000 - args.grid.allscreen_w / 2,
  source_y: 2000 - args.grid.allscreen_h / 2,
  source_w: args.grid.allscreen_w,
  source_h: args.grid.allscreen_h
}

Low-Resolution (Pixel Art) Display

Scale a tiny virtual canvas up to fill the screen — classic for pixel art games:

GAME_W = 84; GAME_H = 48   # virtual resolution (e.g. Nokia 3310)
SCREEN_W = 1280; SCREEN_H = 720
ZOOM = [SCREEN_W / GAME_W, SCREEN_H / GAME_H].min
OFFSET_X = (SCREEN_W - GAME_W * ZOOM) / 2
OFFSET_Y = (SCREEN_H - GAME_H * ZOOM) / 2

def tick(args)
  # Draw at native resolution into target:
  args.outputs[:game].transient!
  args.outputs[:game].width  = GAME_W
  args.outputs[:game].height = GAME_H
  args.outputs[:game].background_color = [199, 240, 216]
  # ... all game rendering goes here at GAME_W x GAME_H coords

  # Scale up to screen:
  args.outputs.sprites << {
    x: OFFSET_X, y: OFFSET_Y,
    w: GAME_W * ZOOM, h: GAME_H * ZOOM,
    path: :game
  }
end

# Translate mouse input from screen space back to game space:
def game_mouse(args)
  { x: (args.inputs.mouse.x - OFFSET_X).idiv(ZOOM),
    y: (args.inputs.mouse.y - OFFSET_Y).idiv(ZOOM) }
end

Thick Lines

DragonRuby only draws 1px lines natively. Emulate thick lines with a rotated solid:

def thick_line(x:, y:, x2:, y2:, thickness: 3, r: 255, g: 255, b: 255, a: 255)
  dx = x2 - x; dy = y2 - y
  length = Math.sqrt(dx*dx + dy*dy)
  angle  = Math.atan2(dy, dx).to_degrees
  {
    x: x, y: y,
    w: length, h: thickness,
    angle: angle, angle_anchor_x: 0, angle_anchor_y: 0.5,
    path: :solid,
    r: r, g: g, b: b, a: a
  }
end

args.outputs.sprites << thick_line(x: 100, y: 100, x2: 400, y2: 300, thickness: 4)

Repeating / Tiled Textures

def tiled_background(args, path, dest_x:, dest_y:, dest_w:, dest_h:)
  sw, sh = args.gtk.calcspritebox(path)
  rt_name = :"tile_#{path}"
  if Kernel.tick_count == 0
    args.outputs[rt_name].width  = dest_w
    args.outputs[rt_name].height = dest_h
    cols = (dest_w.fdiv(sw)).ceil + 1
    rows = (dest_h.fdiv(sh)).ceil + 1
    args.outputs[rt_name].sprites << rows.map { |r|
      cols.map { |c|
        { x: c * sw, y: dest_h - (r + 1) * sh, w: sw, h: sh, path: path }
      }
    }
  end
  { x: dest_x, y: dest_y, w: dest_w, h: dest_h, path: rt_name }
end

args.outputs.sprites << tiled_background(args, 'sprites/grass.png',
                                          dest_x: 0, dest_y: 0,
                                          dest_w: 1280, dest_h: 720)

Coordinate System Origin Switching

args.grid.origin_center!        # (0,0) at screen centre
args.grid.origin_bottom_left!   # (0,0) at bottom-left (default)

Render targets inherit the current origin mode.

Tile-Based Large Maps

Only load the 9 tiles surrounding the player's position (3×3 grid of chunks):

CHUNK = 640
chunk_x = player.x.idiv(CHUNK)
chunk_y = player.y.idiv(CHUNK)
offset_x = 960 - (player.x - chunk_x * CHUNK)
offset_y = 960 - (player.y - chunk_y * CHUNK)

(-1..1).each do |cx|
  (-1..1).each do |cy|
    path = :"chunk_#{chunk_x + cx}_#{chunk_y + cy}"
    args.outputs.sprites << {
      x: offset_x + cx * CHUNK,
      y: offset_y + cy * CHUNK,
      w: CHUNK, h: CHUNK,
      path: path
    }
  end
end

Screenshots

args.outputs.screenshots << {
  filename: "screenshots/shot_#{Kernel.tick_count}.png",
  x: 0, y: 0, w: 1280, h: 720,
  chroma_key: { r: 0, g: 255, b: 0 }   # optional: make green transparent
}

Static Sprites for Performance

# Add once; objects are held by reference and mutated in place:
if Kernel.tick_count == 0
  args.state.stars = 500.map { { x: rand(1280), y: rand(720), w: 2, h: 2, path: :solid, r: 255, g: 255, b: 255 } }
  args.outputs.static_sprites << args.state.stars
end

# Update positions without re-adding:
args.state.stars.each { |s| s[:x] = (s[:x] + 0.5) % 1280 }

Shaders (Indie/Pro)

args.outputs.sprites << {
  x: 0, y: 0, w: 1280, h: 720,
  path: 'sprites/scene.png',
  shader: 'shaders/scanlines.glsl',
  shader_tex1: :render_target_name,
  # Uniforms:
  shader_u_time: Kernel.tick_count / 60.0
}

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

dragonruby

No summary provided by upstream source.

Repository SourceNeeds Review
General

dragonruby-yard

No summary provided by upstream source.

Repository SourceNeeds Review
General

dragonruby-audio

No summary provided by upstream source.

Repository SourceNeeds Review