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.
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glslkit
4
+ module WebGL
5
+ VERSION = "0.1.0.pre"
6
+ end
7
+ 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