Class: Ashton::SignedDistanceField

Inherits:
Object
  • Object
show all
Defined in:
lib/ashton/signed_distance_field.rb

Constant Summary collapse

ZERO_DISTANCE =

color channel containing 0 => -128, 128 => 0, 129 => 1, 255 => 128

128

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(width, height, max_distance, options = {}, &block) ⇒ SignedDistanceField

Creates a Signed Distance Field based on a given image. When drawing into the SDF, drawing should ONLY have alpha of 0 (clear) or 255 (solid)

Parameters:

  • width (Integer)
  • height (Integer)
  • max_distance (Integer)

    Maximum distance to measure.

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :step_size (Integer) — default: 1

    pixels to step out.

  • :scale (Integer) — default: 1

    Scale relative to the image.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/ashton/signed_distance_field.rb', line 15

def initialize(width, height, max_distance, options = {}, &block)
  options = {
     scale: 1,
     step_size: 1,
  }.merge! options

  @width, @height = width, height
  @scale = options[:scale].to_f

  @shader = Shader.new fragment: :signed_distance_field, uniforms: {
      max_distance: max_distance.ceil,
      step_size: options[:step_size].floor, # One pixel.
      texture_size: [width, height].map(&:to_f),
  }

  @field = Texture.new (width / @scale).ceil, (height / @scale).ceil
  @mask = Texture.new @field.width, @field.height

  if block_given?
    render_field(&block)
  else
    @field.clear color: Gosu::Color.rgb(*([ZERO_DISTANCE + max_distance] * 3))
  end
end

Instance Attribute Details

#heightObject (readonly)

Returns the value of attribute height.



5
6
7
# File 'lib/ashton/signed_distance_field.rb', line 5

def height
  @height
end

#widthObject (readonly)

Returns the value of attribute width.



5
6
7
# File 'lib/ashton/signed_distance_field.rb', line 5

def width
  @width
end

Instance Method Details

#draw(x, y, z, options = {}) ⇒ Object

Draw the field, usually for debugging purposes.

See Also:

  • Texture#draw


129
130
131
132
133
134
135
136
137
138
139
# File 'lib/ashton/signed_distance_field.rb', line 129

def draw(x, y, z, options = {})
  options = {
      mode: :add,
  }.merge! options

  $window.scale @scale do
    @field.draw x, y, z, options
  end

  nil
end

#line_of_sight?(x1, y1, x2, y2) ⇒ Boolean

Does the point x1, x2 have line of sight to x2, y2 (that is, no solid in the way).

Returns:

  • (Boolean)


78
79
80
# File 'lib/ashton/signed_distance_field.rb', line 78

def line_of_sight?(x1, y1, x2, y2)
  !line_of_sight_blocked_at(x1, y1, x2, y2)
end

#line_of_sight_blocked_at(x1, y1, x2, y2) ⇒ Object

Returns blocking position, else nil if line of sight isn’t blocked.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/ashton/signed_distance_field.rb', line 83

def line_of_sight_blocked_at(x1, y1, x2, y2)
  distance_to_travel = Gosu::distance x1, y1, x2, y2
  distance_x, distance_y = x2 - x1, y2 - y1
  distance_travelled = 0
  x, y = x1, y1

  loop do
    distance = sample_distance x, y

    # Blocked?
    return [x, y] if distance <= 0

    distance_travelled += distance

    # Got to destination in the clear.
    return nil if distance_travelled >= distance_to_travel

    lerp = distance_travelled.fdiv distance_to_travel
    x = x1 + distance_x * lerp
    y = y1 + distance_y * lerp
  end
end

#position_clear?(x, y, radius) ⇒ Boolean

Is the position clear for a given radius around it.

Returns:

  • (Boolean)


41
42
43
# File 'lib/ashton/signed_distance_field.rb', line 41

def position_clear?(x, y, radius)
  sample_distance(x, y) >= radius
end

#render_fieldObject

Update the SDF should the image have changed. Draw the mask in the passed block.

Raises:

  • (ArgumentError)


108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/ashton/signed_distance_field.rb', line 108

def render_field
  raise ArgumentError, "Block required" unless block_given?

  @mask.render do
    @mask.clear
    $window.scale 1.0 / @scale do
      yield self
    end
  end

  @shader.enable do
    @field.render do
      @mask.draw 0, 0, 0
    end
  end

  nil
end

#sample_distance(x, y) ⇒ Object

If positive, distance, in pixels, to the nearest opaque pixel. If negative, distance in pixels to the nearest transparent pixel.



47
48
49
50
51
52
# File 'lib/ashton/signed_distance_field.rb', line 47

def sample_distance(x, y)
  x = [[x, width - 1].min, 0].max
  y = [[y, height - 1].min, 0].max
  # Could be checking any of red/blue/green.
  @field.red((x / @scale).round, (y / @scale).round) - ZERO_DISTANCE
end

#sample_gradient(x, y) ⇒ Float

Gets the gradient of the field at a given point.

Returns:

  • (Float, Float)

    gradient_x, gradient_y



56
57
58
59
60
61
62
63
# File 'lib/ashton/signed_distance_field.rb', line 56

def sample_gradient(x, y)
  d0 = sample_distance x, y - 1
  d1 = sample_distance x - 1, y
  d2 = sample_distance x + 1, y
  d3 = sample_distance x, y + 1

  [(d2 - d1) / @scale, (d3 - d0) / @scale]
end

#sample_normal(x, y) ⇒ Float

Get the normal at a given point.

Returns:

  • (Float, Float)

    normal_x, normal_y



67
68
69
70
71
72
73
74
75
# File 'lib/ashton/signed_distance_field.rb', line 67

def sample_normal(x, y)
  gradient_x, gradient_y = sample_gradient x, y
  length = Gosu::distance 0, 0, gradient_x, gradient_y
  if length == 0
    [0, 0] # This could be NaN in edge cases.
  else
    [gradient_x / length, gradient_y / length]
  end
end

#to_aArray<Array<Integer>>

Convert into a nested array of sample values.

Returns:

  • (Array<Array<Integer>>)


143
144
145
146
147
148
149
# File 'lib/ashton/signed_distance_field.rb', line 143

def to_a
  width.times.map do |x|
    height.times.map do |y|
      sample_distance x, y
    end
  end
end