glslkit-webgl 0.1.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE.txt +21 -0
- data/README.md +98 -0
- data/glslkit-webgl.gemspec +37 -0
- data/lib/glslkit/webgl/context.rb +228 -0
- data/lib/glslkit/webgl/embed.rb +158 -0
- data/lib/glslkit/webgl/errors.rb +39 -0
- data/lib/glslkit/webgl/geometry.rb +64 -0
- data/lib/glslkit/webgl/live_reload.rb +107 -0
- data/lib/glslkit/webgl/matrix.rb +77 -0
- data/lib/glslkit/webgl/program.rb +205 -0
- data/lib/glslkit/webgl/reload_result.rb +28 -0
- data/lib/glslkit/webgl/texture.rb +27 -0
- data/lib/glslkit/webgl/version.rb +7 -0
- data/lib/glslkit/webgl.rb +48 -0
- data/sample/app.rb +52 -0
- data/sample/error_panel.rb +68 -0
- data/sample/generated/cube_shaders.rb +82 -0
- data/sample/generated/neon_shaders.rb +162 -0
- data/sample/generated-broken/neon-broken_shaders.rb +162 -0
- data/sample/index.html +15 -0
- data/sample/neon-error.html +82 -0
- data/sample/neon-error.rb +119 -0
- data/sample/neon.html +22 -0
- data/sample/neon.rb +22 -0
- data/sample/shaders/common/color.glsl +5 -0
- data/sample/shaders/common/sdf.glsl +11 -0
- data/sample/shaders/cube.frag +8 -0
- data/sample/shaders/cube.vert +9 -0
- data/sample/shaders/neon.frag +79 -0
- data/sample/shaders/neon.vert +5 -0
- data/sample/shaders-broken/common/color.glsl +5 -0
- data/sample/shaders-broken/common/sdf.glsl +11 -0
- data/sample/shaders-broken/neon-broken.frag +79 -0
- data/sample/shaders-broken/neon-broken.vert +5 -0
- data/shim/glslkit-webgl.js +49 -0
- metadata +105 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
module WebGL
|
|
5
|
+
class Geometry
|
|
6
|
+
attr_reader :vao, :count, :indexed, :index_type, :mode, :attribute_locations
|
|
7
|
+
|
|
8
|
+
def initialize(gl, program, attributes:, indices: nil, mode: nil)
|
|
9
|
+
@gl = gl
|
|
10
|
+
@vao = gl.call(:createVertexArray)
|
|
11
|
+
@mode = mode || gl[:TRIANGLES]
|
|
12
|
+
# M11d: 構築時に実際に使ったattributeのlocationを保持する
|
|
13
|
+
# (SPEC-livereload.md §4.2)。reload時、Contextがこれと新Programの
|
|
14
|
+
# locationを突き合わせ、このGeometryのVAOがそのまま使えるかを判断する。
|
|
15
|
+
@attribute_locations = {}
|
|
16
|
+
gl.call(:bindVertexArray, @vao)
|
|
17
|
+
build_attributes(program, attributes)
|
|
18
|
+
build_indices(indices)
|
|
19
|
+
gl.call(:bindVertexArray, JS::Null)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def build_attributes(program, attributes)
|
|
25
|
+
float32 = JS.global[:Float32Array]
|
|
26
|
+
counts = []
|
|
27
|
+
attributes.each do |name, config|
|
|
28
|
+
location = program.attribute_locations.fetch(name.to_sym) do
|
|
29
|
+
raise KeyError, "attribute not found in manifest: #{name}"
|
|
30
|
+
end
|
|
31
|
+
@attribute_locations[name.to_sym] = location
|
|
32
|
+
next if location.to_i < 0
|
|
33
|
+
|
|
34
|
+
components = config.fetch(:components)
|
|
35
|
+
data = float32.call(:from, config.fetch(:data).to_js)
|
|
36
|
+
buffer = @gl.call(:createBuffer)
|
|
37
|
+
@gl.call(:bindBuffer, @gl[:ARRAY_BUFFER], buffer)
|
|
38
|
+
@gl.call(:bufferData, @gl[:ARRAY_BUFFER], data, @gl[:STATIC_DRAW])
|
|
39
|
+
@gl.call(:enableVertexAttribArray, location)
|
|
40
|
+
@gl.call(:vertexAttribPointer, location, components, @gl[:FLOAT], false, 0, 0)
|
|
41
|
+
counts << data[:length].to_i / components
|
|
42
|
+
end
|
|
43
|
+
@count = counts.min || 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build_indices(indices)
|
|
47
|
+
@indexed = !indices.nil?
|
|
48
|
+
return unless @indexed
|
|
49
|
+
|
|
50
|
+
max = indices.max || 0
|
|
51
|
+
constructor, @index_type = if max > 65_535
|
|
52
|
+
[JS.global[:Uint32Array], @gl[:UNSIGNED_INT]]
|
|
53
|
+
else
|
|
54
|
+
[JS.global[:Uint16Array], @gl[:UNSIGNED_SHORT]]
|
|
55
|
+
end
|
|
56
|
+
data = constructor.call(:from, indices.to_js)
|
|
57
|
+
buffer = @gl.call(:createBuffer)
|
|
58
|
+
@gl.call(:bindBuffer, @gl[:ELEMENT_ARRAY_BUFFER], buffer)
|
|
59
|
+
@gl.call(:bufferData, @gl[:ELEMENT_ARRAY_BUFFER], data, @gl[:STATIC_DRAW])
|
|
60
|
+
@count = data[:length].to_i
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# M11e: development専用のライブリロード監視ループ(SPEC-livereload.md §4.4)。
|
|
4
|
+
# `glslkit/webgl` からは自動でrequireされない — 使う側が明示的に
|
|
5
|
+
# `require "glslkit/webgl/live_reload"` すること(決定3)。production相当の
|
|
6
|
+
# 環境ではこのファイル自体をrequireしないことで対処する。エンドポイントが
|
|
7
|
+
# 存在しない場合の404ハンドリングは不要(そもそも呼ばれない)。
|
|
8
|
+
#
|
|
9
|
+
# spike/09-fetch.htmlの実機確認(SPEC-livereload.md §4.4)により、
|
|
10
|
+
# `JS::Object#await` はここでは使えないことが分かっている。`.then` + Proc
|
|
11
|
+
# だけで組む。
|
|
12
|
+
require "json"
|
|
13
|
+
|
|
14
|
+
module Glslkit
|
|
15
|
+
module WebGL
|
|
16
|
+
class Context
|
|
17
|
+
LIVE_RELOAD_POLL_INTERVAL_MS = 500
|
|
18
|
+
LIVE_RELOAD_DIGESTS_PATH = "/glslkit/digests.json"
|
|
19
|
+
|
|
20
|
+
# ctx.live_reload("neon", on_error: ->(e) { ... }, on_reload: -> { ... })
|
|
21
|
+
#
|
|
22
|
+
# 決定4: 1ページにつきsetIntervalは1本。2回目以降の呼び出しは監視対象の
|
|
23
|
+
# リストに追加するだけで、既存のポーリングに乗せる。
|
|
24
|
+
def live_reload(name, on_error: nil, on_reload: nil)
|
|
25
|
+
@live_reload_watches ||= {}
|
|
26
|
+
@live_reload_watches[name.to_s] = {digest: nil, on_error: on_error, on_reload: on_reload}
|
|
27
|
+
start_live_reload_polling unless @live_reload_interval_id
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def start_live_reload_polling
|
|
34
|
+
tick = proc { poll_live_reload_digests }
|
|
35
|
+
@live_reload_interval_id = JS.global.call(:setInterval, tick, LIVE_RELOAD_POLL_INTERVAL_MS)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def poll_live_reload_digests
|
|
39
|
+
fetch_json(LIVE_RELOAD_DIGESTS_PATH,
|
|
40
|
+
on_success: ->(digests) { apply_live_reload_digests(digests) },
|
|
41
|
+
on_error: ->(error) { @live_reload_watches.each_value { |watch| watch[:on_error]&.call(error) } })
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# digestが変わっていないプログラムは本体を取得しない。
|
|
45
|
+
def apply_live_reload_digests(digests)
|
|
46
|
+
@live_reload_watches.each do |name, watch|
|
|
47
|
+
new_digest = digests[name]
|
|
48
|
+
next if new_digest.nil? || new_digest == watch[:digest]
|
|
49
|
+
|
|
50
|
+
fetch_and_reload_program(name, watch)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fetch_and_reload_program(name, watch)
|
|
55
|
+
fetch_json("/glslkit/programs/#{name}.json",
|
|
56
|
+
on_success: ->(payload) { apply_live_reload_payload(name, watch, payload) },
|
|
57
|
+
on_error: ->(error) { watch[:on_error]&.call(error) })
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 失敗時に既存の状態を壊さないこと(§4.1)。reload_programが
|
|
61
|
+
# CompileError/LinkError/ReloadIncompatibleErrorを投げても、ここで
|
|
62
|
+
# 捕まえてon_errorへ回すだけで、監視ループ自体は止めない。
|
|
63
|
+
def apply_live_reload_payload(name, watch, payload)
|
|
64
|
+
watch[:digest] = payload["source_digest"]
|
|
65
|
+
|
|
66
|
+
if payload.key?("error")
|
|
67
|
+
watch[:on_error]&.call(payload.fetch("error"))
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
source_maps = payload.fetch("source_maps")
|
|
72
|
+
result = reload_program(name,
|
|
73
|
+
vertex: payload.fetch("vertex"), fragment: payload.fetch("fragment"),
|
|
74
|
+
manifest: payload.fetch("manifest"),
|
|
75
|
+
source_maps: {vertex: source_maps.fetch("vertex"), fragment: source_maps.fetch("fragment")})
|
|
76
|
+
watch[:on_reload]&.call(result)
|
|
77
|
+
rescue => e
|
|
78
|
+
watch[:on_error]&.call(e)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# fetch → .then(response) → text() → .then(text) → JSON.parse という
|
|
82
|
+
# 定型処理をまとめる。ok?がfalseの応答も、rejectされたPromiseと同様に
|
|
83
|
+
# on_errorへ回す。
|
|
84
|
+
def fetch_json(path, on_success:, on_error:)
|
|
85
|
+
on_response = proc do |response|
|
|
86
|
+
if response[:ok] == JS::True
|
|
87
|
+
response.call(:text)
|
|
88
|
+
else
|
|
89
|
+
on_error.call("fetch #{path} failed: status=#{response[:status]}")
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
on_text = proc do |text|
|
|
94
|
+
next if text.nil?
|
|
95
|
+
|
|
96
|
+
begin
|
|
97
|
+
on_success.call(JSON.parse(text.to_s))
|
|
98
|
+
rescue => e
|
|
99
|
+
on_error.call(e)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
on_rejected = proc { |error| on_error.call(error) }
|
|
103
|
+
JS.global.fetch(path).call(:then, on_response).call(:then, on_text).call(:catch, on_rejected)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
module WebGL
|
|
5
|
+
module Matrix
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def identity!(out)
|
|
9
|
+
16.times { |i| out[i] = 0.0 }
|
|
10
|
+
out[0] = out[5] = out[10] = out[15] = 1.0
|
|
11
|
+
out
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def rotation_z!(out, radians)
|
|
15
|
+
cosine = Math.cos(radians)
|
|
16
|
+
sine = Math.sin(radians)
|
|
17
|
+
identity!(out)
|
|
18
|
+
out[0] = cosine
|
|
19
|
+
out[1] = sine
|
|
20
|
+
out[4] = -sine
|
|
21
|
+
out[5] = cosine
|
|
22
|
+
out
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def rotation_y!(out, radians)
|
|
26
|
+
cosine = Math.cos(radians)
|
|
27
|
+
sine = Math.sin(radians)
|
|
28
|
+
identity!(out)
|
|
29
|
+
out[0] = cosine
|
|
30
|
+
out[2] = -sine
|
|
31
|
+
out[8] = sine
|
|
32
|
+
out[10] = cosine
|
|
33
|
+
out
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def translation!(out, x, y, z)
|
|
37
|
+
identity!(out)
|
|
38
|
+
out[12] = x
|
|
39
|
+
out[13] = y
|
|
40
|
+
out[14] = z
|
|
41
|
+
out
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def multiply!(out, left, right)
|
|
45
|
+
4.times do |column|
|
|
46
|
+
offset = column * 4
|
|
47
|
+
r0 = right[offset].to_f
|
|
48
|
+
r1 = right[offset + 1].to_f
|
|
49
|
+
r2 = right[offset + 2].to_f
|
|
50
|
+
r3 = right[offset + 3].to_f
|
|
51
|
+
out[offset] = left[0].to_f * r0 + left[4].to_f * r1 + left[8].to_f * r2 + left[12].to_f * r3
|
|
52
|
+
out[offset + 1] = left[1].to_f * r0 + left[5].to_f * r1 + left[9].to_f * r2 + left[13].to_f * r3
|
|
53
|
+
out[offset + 2] = left[2].to_f * r0 + left[6].to_f * r1 + left[10].to_f * r2 + left[14].to_f * r3
|
|
54
|
+
out[offset + 3] = left[3].to_f * r0 + left[7].to_f * r1 + left[11].to_f * r2 + left[15].to_f * r3
|
|
55
|
+
end
|
|
56
|
+
out
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def perspective!(out, fovy, aspect, near, far)
|
|
60
|
+
f = 1.0 / Math.tan(fovy / 2.0)
|
|
61
|
+
16.times { |i| out[i] = 0.0 }
|
|
62
|
+
out[0] = f / aspect
|
|
63
|
+
out[5] = f
|
|
64
|
+
out[11] = -1.0
|
|
65
|
+
if far
|
|
66
|
+
nf = 1.0 / (near - far)
|
|
67
|
+
out[10] = (far + near) * nf
|
|
68
|
+
out[14] = 2.0 * far * near * nf
|
|
69
|
+
else
|
|
70
|
+
out[10] = -1.0
|
|
71
|
+
out[14] = -2.0 * near
|
|
72
|
+
end
|
|
73
|
+
out
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
module WebGL
|
|
5
|
+
class Program
|
|
6
|
+
LOG_LINE_PATTERNS = [
|
|
7
|
+
/(?:ERROR|WARNING):\s*\d+:(\d+):\s*(.*)/,
|
|
8
|
+
/\b\d+\((\d+)\)\s*:\s*(?:error|warning)?\s*(.*)/i
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :handle, :attribute_locations
|
|
12
|
+
|
|
13
|
+
def initialize(gl, manifest_program, vertex:, fragment:, source_maps: {}, state: nil)
|
|
14
|
+
@gl = gl
|
|
15
|
+
@state = state || {program: nil}
|
|
16
|
+
@manifest_program = manifest_program
|
|
17
|
+
@source_maps = source_maps
|
|
18
|
+
@handle = build(vertex, fragment)
|
|
19
|
+
@uniform_indices = {}
|
|
20
|
+
@uniform_locations = []
|
|
21
|
+
@uniform_setters = []
|
|
22
|
+
@uniform_matrix = []
|
|
23
|
+
@uniform_lengths = []
|
|
24
|
+
@uniform_buffers = []
|
|
25
|
+
prepare_uniforms
|
|
26
|
+
prepare_attributes
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def use
|
|
30
|
+
unless @state[:program].equal?(@handle)
|
|
31
|
+
@gl.call(:useProgram, @handle)
|
|
32
|
+
@state[:program] = @handle
|
|
33
|
+
end
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def set(name, value)
|
|
38
|
+
index = @uniform_indices[name.to_sym]
|
|
39
|
+
raise UnknownUniformError, "uniform not found in manifest: #{name}" unless index
|
|
40
|
+
|
|
41
|
+
location = @uniform_locations[index]
|
|
42
|
+
return self if null_location?(location)
|
|
43
|
+
|
|
44
|
+
use
|
|
45
|
+
buffer = @uniform_buffers[index]
|
|
46
|
+
expected = @uniform_lengths[index]
|
|
47
|
+
copy_value(buffer, value, expected, name)
|
|
48
|
+
setter = @uniform_setters[index]
|
|
49
|
+
if @uniform_matrix[index]
|
|
50
|
+
@gl.call(setter, location, false, buffer)
|
|
51
|
+
else
|
|
52
|
+
@gl.call(setter, location, buffer)
|
|
53
|
+
end
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# M11d: reload時にuniform値を引き継ぐため(SPEC-livereload.md §4.3)の、
|
|
58
|
+
# 名前ごとの現在値スナップショット。{name_sym => {type:, element_count:, values:}}。
|
|
59
|
+
# typeは互換性判定にのみ使う(値そのものには使わない)。
|
|
60
|
+
def uniform_snapshot
|
|
61
|
+
@manifest_program.fetch("uniforms").each_with_object({}) do |uniform, snapshot|
|
|
62
|
+
name = uniform.fetch("name").to_sym
|
|
63
|
+
index = @uniform_indices.fetch(name)
|
|
64
|
+
count = @uniform_lengths[index]
|
|
65
|
+
buffer = @uniform_buffers[index]
|
|
66
|
+
snapshot[name] = {
|
|
67
|
+
type: uniform.fetch("type"),
|
|
68
|
+
element_count: count,
|
|
69
|
+
values: Array.new(count) { |i| buffer[i].to_f }
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# reload後、旧Program(old_program)が持っていたuniform値を自分自身に
|
|
75
|
+
# 復元する。名前・type・element_countがすべて一致するものだけを復元し、
|
|
76
|
+
# 一致しないものは戻り値に集めて呼び出し側(Context)に返す。R4には
|
|
77
|
+
# 反しない(初期化時1回の書き込みであり、毎フレームの再確保ではない)。
|
|
78
|
+
def restore_uniforms_from(old_program)
|
|
79
|
+
new_snapshot = uniform_snapshot
|
|
80
|
+
discarded = []
|
|
81
|
+
|
|
82
|
+
old_program.uniform_snapshot.each do |name, old_entry|
|
|
83
|
+
new_entry = new_snapshot[name]
|
|
84
|
+
if new_entry.nil?
|
|
85
|
+
discarded << {name: name, reason: :removed}
|
|
86
|
+
elsif new_entry[:type] != old_entry[:type] || new_entry[:element_count] != old_entry[:element_count]
|
|
87
|
+
discarded << {name: name, reason: :type_changed, from: old_entry[:type], to: new_entry[:type]}
|
|
88
|
+
else
|
|
89
|
+
set(name, old_entry[:values])
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
discarded
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def build(vertex_source, fragment_source)
|
|
99
|
+
vertex = compile(:vertex, vertex_source, @gl[:VERTEX_SHADER])
|
|
100
|
+
fragment = compile(:fragment, fragment_source, @gl[:FRAGMENT_SHADER])
|
|
101
|
+
program = @gl.call(:createProgram)
|
|
102
|
+
@gl.call(:attachShader, program, vertex)
|
|
103
|
+
@gl.call(:attachShader, program, fragment)
|
|
104
|
+
@gl.call(:linkProgram, program)
|
|
105
|
+
linked = @gl.call(:getProgramParameter, program, @gl[:LINK_STATUS])
|
|
106
|
+
unless js_truthy?(linked)
|
|
107
|
+
raw_log = @gl.call(:getProgramInfoLog, program).to_s
|
|
108
|
+
@gl.call(:deleteProgram, program)
|
|
109
|
+
raise LinkError, raw_log
|
|
110
|
+
end
|
|
111
|
+
@gl.call(:deleteShader, vertex)
|
|
112
|
+
@gl.call(:deleteShader, fragment)
|
|
113
|
+
program
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def compile(stage, source, shader_type)
|
|
117
|
+
shader = @gl.call(:createShader, shader_type)
|
|
118
|
+
@gl.call(:shaderSource, shader, source)
|
|
119
|
+
@gl.call(:compileShader, shader)
|
|
120
|
+
compiled = @gl.call(:getShaderParameter, shader, @gl[:COMPILE_STATUS])
|
|
121
|
+
return shader if js_truthy?(compiled)
|
|
122
|
+
|
|
123
|
+
raw_log = @gl.call(:getShaderInfoLog, shader).to_s
|
|
124
|
+
@gl.call(:deleteShader, shader)
|
|
125
|
+
line, detail = parse_log(raw_log)
|
|
126
|
+
resolved = line && @source_maps[stage]&.resolve(line)
|
|
127
|
+
file, original_line = resolved if resolved
|
|
128
|
+
raise CompileError.new(
|
|
129
|
+
stage: stage,
|
|
130
|
+
raw_log: raw_log,
|
|
131
|
+
file: file,
|
|
132
|
+
line: original_line,
|
|
133
|
+
detail: detail
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def parse_log(raw_log)
|
|
138
|
+
raw_log.each_line do |line|
|
|
139
|
+
LOG_LINE_PATTERNS.each do |pattern|
|
|
140
|
+
match = pattern.match(line)
|
|
141
|
+
return [match[1].to_i, match[2].to_s.strip] if match
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
[nil, nil]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def prepare_uniforms
|
|
148
|
+
float32 = JS.global[:Float32Array]
|
|
149
|
+
int32 = JS.global[:Int32Array]
|
|
150
|
+
uint32 = JS.global[:Uint32Array]
|
|
151
|
+
@manifest_program.fetch("uniforms").each_with_index do |uniform, index|
|
|
152
|
+
name = uniform.fetch("name")
|
|
153
|
+
@uniform_indices[name.to_sym] = index
|
|
154
|
+
@uniform_locations[index] = @gl.call(:getUniformLocation, @handle, name)
|
|
155
|
+
setter = uniform.fetch("setter").to_sym
|
|
156
|
+
@uniform_setters[index] = setter
|
|
157
|
+
@uniform_matrix[index] = uniform.fetch("matrix")
|
|
158
|
+
@uniform_lengths[index] = uniform.fetch("element_count")
|
|
159
|
+
constructor =
|
|
160
|
+
if setter.to_s.end_with?("uiv")
|
|
161
|
+
uint32
|
|
162
|
+
elsif setter.to_s.end_with?("iv")
|
|
163
|
+
int32
|
|
164
|
+
else
|
|
165
|
+
float32
|
|
166
|
+
end
|
|
167
|
+
@uniform_buffers[index] = constructor.new(@uniform_lengths[index])
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def prepare_attributes
|
|
172
|
+
@attribute_locations = {}
|
|
173
|
+
@manifest_program.fetch("attributes").each do |attribute|
|
|
174
|
+
name = attribute.fetch("name")
|
|
175
|
+
location = attribute["location"]
|
|
176
|
+
location = @gl.call(:getAttribLocation, @handle, name).to_i if location.nil?
|
|
177
|
+
@attribute_locations[name.to_sym] = location
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def copy_value(buffer, value, expected, name)
|
|
182
|
+
if expected == 1 && !value.respond_to?(:length)
|
|
183
|
+
buffer[0] = value
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
actual = value[:length].to_i if value.is_a?(JS::Object)
|
|
188
|
+
actual ||= value.length
|
|
189
|
+
unless actual == expected
|
|
190
|
+
raise UniformLengthError,
|
|
191
|
+
"uniform #{name} expects #{expected} elements, got #{actual} from #{value}"
|
|
192
|
+
end
|
|
193
|
+
expected.times { |i| buffer[i] = value[i] }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def null_location?(location)
|
|
197
|
+
location.nil? || location == JS::Null || location == JS::Undefined
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def js_truthy?(value)
|
|
201
|
+
value == true || value == JS::True
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
module WebGL
|
|
5
|
+
# Context#reload_program(M11d、SPEC-livereload.md §4)の戻り値。
|
|
6
|
+
# コンパイル/リンクの失敗(CompileError/LinkError)とattribute locationの
|
|
7
|
+
# 不一致(ReloadIncompatibleError)は例外として投げる — これらは
|
|
8
|
+
# 「差し替え自体が起きなかった」ことを意味し、呼び出し側が旧Programを
|
|
9
|
+
# 使い続けるという単純な話で終わるため。
|
|
10
|
+
#
|
|
11
|
+
# ReloadResultが表すのは、差し替え自体は成功したが、一部のuniform値を
|
|
12
|
+
# 引き継げなかった(名前が消えた/型やelement_countが変わった)という、
|
|
13
|
+
# 差し替え後も動作は継続する軽微な話(§4.3)。診断はerrorにはならない
|
|
14
|
+
# ため、ok?は常にtrueになる想定だが、将来の拡張に備えて診断のseverityを
|
|
15
|
+
# 見て判定する形にしている。
|
|
16
|
+
class ReloadResult
|
|
17
|
+
attr_reader :diagnostics
|
|
18
|
+
|
|
19
|
+
def initialize(diagnostics: [])
|
|
20
|
+
@diagnostics = diagnostics
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ok?
|
|
24
|
+
diagnostics.none? { |d| d.severity == :error }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
module WebGL
|
|
5
|
+
class Texture
|
|
6
|
+
attr_reader :unit
|
|
7
|
+
|
|
8
|
+
def initialize(gl, width:, height:, data:, unit: 0)
|
|
9
|
+
@gl = gl
|
|
10
|
+
@unit = unit
|
|
11
|
+
@handle = gl.call(:createTexture)
|
|
12
|
+
pixels = JS.global[:Uint8Array].call(:from, data.to_js)
|
|
13
|
+
bind
|
|
14
|
+
gl.call(:texImage2D, gl[:TEXTURE_2D], 0, gl[:RGBA], width, height, 0,
|
|
15
|
+
gl[:RGBA], gl[:UNSIGNED_BYTE], pixels)
|
|
16
|
+
gl.call(:texParameteri, gl[:TEXTURE_2D], gl[:TEXTURE_MIN_FILTER], gl[:NEAREST])
|
|
17
|
+
gl.call(:texParameteri, gl[:TEXTURE_2D], gl[:TEXTURE_MAG_FILTER], gl[:NEAREST])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def bind
|
|
21
|
+
@gl.call(:activeTexture, @gl[:TEXTURE0].to_i + @unit)
|
|
22
|
+
@gl.call(:bindTexture, @gl[:TEXTURE_2D], @handle)
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "webgl/version"
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "js"
|
|
7
|
+
rescue LoadError => error
|
|
8
|
+
raise LoadError,
|
|
9
|
+
"glslkit-webgl requires a ruby.wasm runtime (the `js` gem, which ships with " \
|
|
10
|
+
"ruby.wasm's browser builds, could not be loaded). This gem cannot run under " \
|
|
11
|
+
"a normal CRuby/JRuby/TruffleRuby install — it only runs inside a browser " \
|
|
12
|
+
"via ruby.wasm. See https://github.com/ruby/ruby.wasm for how to set that up.",
|
|
13
|
+
error.backtrace
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# glslkit-webgl は core の狭い入口(digestをロードしない)を使う。
|
|
17
|
+
# 前処理・解析・digest計算が要る `require "glslkit"` は使わないこと(M8g)。
|
|
18
|
+
begin
|
|
19
|
+
require "glslkit/runtime"
|
|
20
|
+
rescue LoadError => error
|
|
21
|
+
raise unless error.path == "glslkit/runtime"
|
|
22
|
+
|
|
23
|
+
# Source-tree browser samples are loaded by JS::RequireRemote rather than an
|
|
24
|
+
# installed gem. Packaged users take the normal require path above.
|
|
25
|
+
require_relative "../../../core/lib/glslkit/runtime"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
require_relative "webgl/errors"
|
|
29
|
+
require_relative "webgl/matrix"
|
|
30
|
+
require_relative "webgl/reload_result"
|
|
31
|
+
require_relative "webgl/program"
|
|
32
|
+
require_relative "webgl/geometry"
|
|
33
|
+
require_relative "webgl/texture"
|
|
34
|
+
require_relative "webgl/context"
|
|
35
|
+
|
|
36
|
+
module Glslkit
|
|
37
|
+
module WebGL
|
|
38
|
+
class << self
|
|
39
|
+
attr_accessor :debug
|
|
40
|
+
|
|
41
|
+
def context(selector)
|
|
42
|
+
Context.from_selector(selector)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
self.debug = false
|
|
47
|
+
end
|
|
48
|
+
end
|
data/sample/app.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../lib/glslkit/webgl"
|
|
4
|
+
require_relative "generated/cube_shaders"
|
|
5
|
+
|
|
6
|
+
POSITIONS = [
|
|
7
|
+
-1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1,
|
|
8
|
+
-1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1
|
|
9
|
+
].freeze
|
|
10
|
+
UVS = [0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1].freeze
|
|
11
|
+
INDICES = [
|
|
12
|
+
0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6,
|
|
13
|
+
0, 4, 5, 0, 5, 1, 3, 2, 6, 3, 6, 7,
|
|
14
|
+
1, 5, 6, 1, 6, 2, 0, 3, 7, 0, 7, 4
|
|
15
|
+
].freeze
|
|
16
|
+
PIXELS = [
|
|
17
|
+
255, 80, 120, 255, 40, 210, 255, 255,
|
|
18
|
+
40, 210, 255, 255, 255, 80, 120, 255
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
ctx = Glslkit::WebGL.context("#canvas")
|
|
22
|
+
program = ctx.program(Glslkit::Manifest.parse(CubeShaders::MANIFEST), "cube",
|
|
23
|
+
vertex: CubeShaders::VERTEX, fragment: CubeShaders::FRAGMENT,
|
|
24
|
+
source_maps: {
|
|
25
|
+
vertex: Glslkit::SourceMap.from_h(CubeShaders::SOURCE_MAPS[:vertex]),
|
|
26
|
+
fragment: Glslkit::SourceMap.from_h(CubeShaders::SOURCE_MAPS[:fragment])
|
|
27
|
+
})
|
|
28
|
+
geometry = ctx.geometry(program: program, attributes: {
|
|
29
|
+
a_position: {data: POSITIONS, components: 3}, a_uv: {data: UVS, components: 2}
|
|
30
|
+
}, indices: INDICES)
|
|
31
|
+
texture = ctx.texture2d(width: 2, height: 2, data: PIXELS, unit: 0)
|
|
32
|
+
|
|
33
|
+
projection = Array.new(16, 0.0)
|
|
34
|
+
view = Array.new(16, 0.0)
|
|
35
|
+
model = Array.new(16, 0.0)
|
|
36
|
+
view_model = Array.new(16, 0.0)
|
|
37
|
+
mvp = Array.new(16, 0.0)
|
|
38
|
+
Glslkit::WebGL::Matrix.perspective!(projection, Math::PI / 3.0, 1.0, 0.1, 100.0)
|
|
39
|
+
Glslkit::WebGL::Matrix.translation!(view, 0.0, 0.0, -4.0)
|
|
40
|
+
|
|
41
|
+
ctx.viewport
|
|
42
|
+
ctx.depth_test = true
|
|
43
|
+
program.set(:u_texture, texture.unit)
|
|
44
|
+
ctx.loop do |seconds|
|
|
45
|
+
Glslkit::WebGL::Matrix.rotation_y!(model, seconds)
|
|
46
|
+
Glslkit::WebGL::Matrix.multiply!(view_model, view, model)
|
|
47
|
+
Glslkit::WebGL::Matrix.multiply!(mvp, projection, view_model)
|
|
48
|
+
program.set(:u_mvp, mvp)
|
|
49
|
+
texture.bind
|
|
50
|
+
ctx.clear(red: 0.04, green: 0.05, blue: 0.09)
|
|
51
|
+
ctx.draw(geometry)
|
|
52
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Glslkit::WebGL::CompileErrorから画面表示用のデータを組み立てる。DOMには
|
|
4
|
+
# 一切触れない(webgl/test でDOM無しにユニットテストできるようにするため)。
|
|
5
|
+
# `file`/`line`はresolveできなかった場合nilになりうる(M10a修正2)。
|
|
6
|
+
# ここで必ずフォールバック文字列に変換し、呼び出し側(neon-error.rb)が
|
|
7
|
+
# nilを直接扱わなくて済むようにする。
|
|
8
|
+
module NeonErrorPanel
|
|
9
|
+
UNRESOLVED_FILE = "(unresolved — driver did not report a line inside a mapped segment)"
|
|
10
|
+
UNRESOLVED_LINE = "(unresolved)"
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def describe(error)
|
|
15
|
+
{
|
|
16
|
+
stage: error.stage.to_s,
|
|
17
|
+
file: error.file || UNRESOLVED_FILE,
|
|
18
|
+
resolved: !error.file.nil?,
|
|
19
|
+
line: error.line ? error.line.to_s : UNRESOLVED_LINE,
|
|
20
|
+
message: error.message.to_s,
|
|
21
|
+
raw_log: error.raw_log.to_s
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# M11f: ctx.live_reload の on_error に渡ってくる値は一様ではない
|
|
26
|
+
# (SPEC-livereload.md §4.4/§4.5)。
|
|
27
|
+
# - Glslkit::WebGL::CompileError / LinkError / ReloadIncompatibleError
|
|
28
|
+
# (クライアント側でreload_programが投げた例外)
|
|
29
|
+
# - Hash({"kind" => "preprocess" | "validation", ...}、サーバ側の
|
|
30
|
+
# programs/:name.json がコンパイル前に落ちたことを報告するペイロード。
|
|
31
|
+
# M11cの§3.4参照)
|
|
32
|
+
# - それ以外(ネットワーク断など、文字列やJSの値)
|
|
33
|
+
# これらを1つの表示用ハッシュに正規化する。DOMには一切触れない
|
|
34
|
+
# (describeと同じ理由)。
|
|
35
|
+
def describe_live_reload_error(error)
|
|
36
|
+
case error
|
|
37
|
+
when Glslkit::WebGL::CompileError
|
|
38
|
+
describe(error).merge(kind: "compile")
|
|
39
|
+
when Glslkit::WebGL::LinkError
|
|
40
|
+
{kind: "link", message: "shader link failed", raw_log: error.raw_log.to_s}
|
|
41
|
+
when Glslkit::WebGL::ReloadIncompatibleError
|
|
42
|
+
{kind: "incompatible", message: error.message.to_s}
|
|
43
|
+
when Hash
|
|
44
|
+
describe_server_payload(error)
|
|
45
|
+
else
|
|
46
|
+
{kind: "unknown", message: error.to_s}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def describe_server_payload(payload)
|
|
51
|
+
case payload["kind"]
|
|
52
|
+
when "validation"
|
|
53
|
+
diagnostics = payload.fetch("diagnostics", []).map { |d| describe_diagnostic(d) }
|
|
54
|
+
{kind: "validation", diagnostics: diagnostics}
|
|
55
|
+
when "preprocess"
|
|
56
|
+
{kind: "preprocess", message: payload["message"].to_s, error_class: payload["class"].to_s}
|
|
57
|
+
else
|
|
58
|
+
{kind: "unknown", message: payload.inspect}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def describe_diagnostic(diagnostic)
|
|
63
|
+
{
|
|
64
|
+
severity: diagnostic["severity"].to_s, code: diagnostic["code"].to_s, message: diagnostic["message"].to_s,
|
|
65
|
+
file: diagnostic["file"] || UNRESOLVED_FILE, line: diagnostic["line"] ? diagnostic["line"].to_s : UNRESOLVED_LINE
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|