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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6ae64d28127a6a9f72c2c3017ba1b7e11739e7f374634be7e2a6a6eecbf663a7
|
|
4
|
+
data.tar.gz: 152a82f8f5be95b90b2d15a8c9b5976d55737164e7d6b69060b89433cfcb56da
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c97c9dcbd8d95bd2257241915974ee053994659281f49da8e78df7a4a2b594e377587410d31a7008f6ec994a3ee0b8c91be93a05339e93aecce6ad881409053b
|
|
7
|
+
data.tar.gz: 16c71ae9f9f16e146c5d82b4e4e8fbdab1af3ca6e584ab263576c87a929d88503d02d729758ff5ae18ead15a67dcdb75a627e40fb0ea68e5c0675b0e971ea5a1
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
初回リリース (v0.1)。**ruby.wasm 環境専用。** 通常の CRuby/JRuby/TruffleRuby
|
|
6
|
+
にはインストールできるが、`require "js"` に依存するため実行できない。
|
|
7
|
+
|
|
8
|
+
### 追加
|
|
9
|
+
|
|
10
|
+
- `Glslkit::WebGL::Context` — `glslkit` の reflection マニフェストから
|
|
11
|
+
Program/Geometry/Textureを構築。`getActiveUniform`/`getActiveAttrib` を
|
|
12
|
+
呼ばない。`ctx.loop` は `requestAnimationFrame` へ Ruby の `Proc` を直接渡す
|
|
13
|
+
- `Glslkit::WebGL::Program` — uniform設定は再利用する型付き配列に書き込み、
|
|
14
|
+
毎フレームの新規確保を避ける。コンパイル/リンクエラーは
|
|
15
|
+
`Glslkit::WebGL::CompileError`/`LinkError` として、`SourceMap` 経由で
|
|
16
|
+
元ファイル名・行番号付きで発生する
|
|
17
|
+
- `Glslkit::WebGL::Geometry` — 頂点データは構築時に型付き配列へ一括転送
|
|
18
|
+
(`Array#to_js` → `Float32Array.from`)。ミューテーションAPIは提供しない
|
|
19
|
+
- `Glslkit::WebGL::Texture`、破壊的更新の行列ヘルパ (`Glslkit::WebGL::Matrix`)
|
|
20
|
+
- `Glslkit::WebGL::Embed`(`rake glslkit:embed`)— `.vert`/`.frag` から
|
|
21
|
+
マニフェスト・SourceMap込みのデータ専用Rubyモジュールを生成し、ホスト側で
|
|
22
|
+
wasmに埋め込めるようにする
|
|
23
|
+
- `Context#reload_program` / `#program_for` — 実行中のシェーダを
|
|
24
|
+
Geometry・uniform値を保ったまま差し替える。attribute locationが変わる等の
|
|
25
|
+
互換性が無い変更は `ReloadIncompatibleError` を投げる
|
|
26
|
+
- `Glslkit::WebGL::LiveReload`(`require "glslkit/webgl/live_reload"`、
|
|
27
|
+
明示requireでのみ有効)— `glslkit-rails` のライブリロードエンドポイントを
|
|
28
|
+
ポーリングし、変更を検知したら自動で `reload_program` を呼ぶ
|
|
29
|
+
- サンプル: 回転する立方体 (`sample/index.html`)、レイマーチングデモ
|
|
30
|
+
(`sample/neon.html`)、include先のコンパイルエラーが元ファイル名・行番号を
|
|
31
|
+
指すことを示すデモ (`sample/neon-error.html`)
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kyubey1228
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# glslkit-webgl
|
|
2
|
+
|
|
3
|
+
`glslkit-webgl` is a WebGL2 runtime for Ruby programs running in a browser with
|
|
4
|
+
ruby.wasm. It consumes a `glslkit` reflection manifest, caches WebGL references,
|
|
5
|
+
and reports shader compiler errors as Ruby exceptions.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
Serve this repository over HTTP and open `webgl/sample/index.html`. The sample
|
|
10
|
+
draws a rotating, textured cube without application JavaScript, built from
|
|
11
|
+
`webgl/sample/shaders/cube.{vert,frag}` via `rake glslkit:embed`.
|
|
12
|
+
`webgl/sample/neon.html` is an animated fragment-shader raymarching demo, built
|
|
13
|
+
from `webgl/sample/shaders/neon.*` the same way.
|
|
14
|
+
`webgl/sample/neon-error.html` intentionally breaks `common/sdf.glsl` (an
|
|
15
|
+
`#include`d file, not the top-level shader) to demonstrate that `CompileError`
|
|
16
|
+
reports the original included file and line, with a button to switch back to
|
|
17
|
+
the working shaders and start rendering.
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<canvas id="canvas" width="640" height="480"></canvas>
|
|
21
|
+
<script src="/path/to/glslkit-webgl.js" data-entry="./app.rb"
|
|
22
|
+
data-ruby-version="4.0" data-wasm-wasi-version="2.10.1"></script>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The shim only starts ruby.wasm. `JS::RequireRemote` fetches Ruby files and all
|
|
26
|
+
rendering logic remains in Ruby. `window.glslkitWebGLReady` settles when the entry
|
|
27
|
+
file finishes loading. Packaged wasm builds should install both gems normally.
|
|
28
|
+
|
|
29
|
+
## Runtime API
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require "glslkit/webgl"
|
|
33
|
+
|
|
34
|
+
ctx = Glslkit::WebGL.context("#canvas")
|
|
35
|
+
manifest = Glslkit::Manifest.parse(MANIFEST_JSON)
|
|
36
|
+
program = ctx.program(manifest, "main", vertex: VERTEX_SOURCE,
|
|
37
|
+
fragment: FRAGMENT_SOURCE,
|
|
38
|
+
source_maps: {vertex: vertex_map, fragment: fragment_map})
|
|
39
|
+
geometry = ctx.geometry(program: program, attributes: {
|
|
40
|
+
a_position: {data: positions, components: 3},
|
|
41
|
+
a_uv: {data: uvs, components: 2}
|
|
42
|
+
}, indices: indices)
|
|
43
|
+
texture = ctx.texture2d(width: 2, height: 2, data: rgba_pixels, unit: 0)
|
|
44
|
+
|
|
45
|
+
matrix = Array.new(16, 0.0)
|
|
46
|
+
ctx.loop do |seconds|
|
|
47
|
+
Glslkit::WebGL::Matrix.rotation_z!(matrix, seconds)
|
|
48
|
+
program.set(:u_transform, matrix)
|
|
49
|
+
program.set(:u_texture, texture.unit)
|
|
50
|
+
texture.bind
|
|
51
|
+
ctx.draw(geometry)
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Unknown uniforms and incorrect `element_count` values raise immediately.
|
|
56
|
+
Locations optimized away by GLSL are silently ignored. Geometry data is uploaded
|
|
57
|
+
once with typed-array `from`; there is intentionally no mutation API.
|
|
58
|
+
|
|
59
|
+
## Shader errors and source maps
|
|
60
|
+
|
|
61
|
+
Pass each stage's `Glslkit::SourceMap` through `source_maps:`. Recognized driver
|
|
62
|
+
line numbers are resolved to the original included file. `CompileError` exposes
|
|
63
|
+
`stage`, `file`, `line`, and `raw_log`. Unknown log formats still raise, with
|
|
64
|
+
`file` and `line` set to `nil`. Link failures raise `LinkError`. JS log values are
|
|
65
|
+
converted with `#to_s` before parsing.
|
|
66
|
+
|
|
67
|
+
## Debug mode
|
|
68
|
+
|
|
69
|
+
Set `Glslkit::WebGL.debug = true` to call `getError` once at each frame end. It
|
|
70
|
+
defaults to `false` and should not remain enabled because `getError` synchronizes
|
|
71
|
+
the WebGL command queue.
|
|
72
|
+
|
|
73
|
+
## Performance and scope limits
|
|
74
|
+
|
|
75
|
+
The following CPU-side workloads are unsupported in v0.1:
|
|
76
|
+
|
|
77
|
+
- replacing vertex data every frame;
|
|
78
|
+
- CPU skinning;
|
|
79
|
+
- CPU morph-target interpolation;
|
|
80
|
+
- CPU-updated particle geometry.
|
|
81
|
+
|
|
82
|
+
Transferring 100,000 floats took 18.8 ms in the design measurements, exceeding a
|
|
83
|
+
60 fps frame budget. Keep geometry static, deform vertices in shaders, and send
|
|
84
|
+
only matrices and weights through uniforms. Per-frame bone-matrix calculation
|
|
85
|
+
cost remains unmeasured.
|
|
86
|
+
|
|
87
|
+
The runtime is WebGL2-only. It has no WebGL1 fallback, scene graph, material
|
|
88
|
+
system, or glTF/VRM loader.
|
|
89
|
+
|
|
90
|
+
## JS value safety (R6)
|
|
91
|
+
|
|
92
|
+
Never call `#class` on a value that may be a `JS::Object`; JavaScript primitives
|
|
93
|
+
can fail in `Reflect.has`. Use `is_a?(JS::Object)` for checks and `#inspect`,
|
|
94
|
+
`#typeof`, or `#to_s` for display. A test scans implementation Ruby files for
|
|
95
|
+
accidental `.class` calls.
|
|
96
|
+
|
|
97
|
+
Normal CRuby fails immediately with a clear message because ruby.wasm's `js`
|
|
98
|
+
library is required.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/glslkit/webgl/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "glslkit-webgl"
|
|
7
|
+
spec.version = Glslkit::WebGL::VERSION
|
|
8
|
+
spec.authors = ["kyubey1228"]
|
|
9
|
+
spec.email = ["kyuuka1228@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "ruby.wasm WebGL2 binding that consumes glslkit reflection manifests"
|
|
12
|
+
spec.description = "Lets you write WebGL2 rendering code entirely in Ruby, running under " \
|
|
13
|
+
"ruby.wasm in the browser. Resolves uniform/attribute locations from a glslkit " \
|
|
14
|
+
"reflection manifest instead of calling getActiveUniform, and surfaces shader " \
|
|
15
|
+
"compile errors as Ruby exceptions pointing at the original file and line. " \
|
|
16
|
+
"REQUIRES a ruby.wasm runtime (the `js` gem) to run — it installs under a normal " \
|
|
17
|
+
"CRuby/JRuby/TruffleRuby, but raises a clear LoadError instead of working there. " \
|
|
18
|
+
"This gem is meant to be bundled into a ruby.wasm build (see README), not required " \
|
|
19
|
+
"from a regular Ruby process."
|
|
20
|
+
spec.homepage = "https://github.com/kyubey1228/glslkit"
|
|
21
|
+
spec.license = "MIT"
|
|
22
|
+
spec.required_ruby_version = ">= 3.1"
|
|
23
|
+
|
|
24
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
25
|
+
spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/main/webgl"
|
|
26
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/webgl/CHANGELOG.md"
|
|
27
|
+
|
|
28
|
+
spec.files = Dir.glob("lib/**/*.rb") + Dir.glob("shim/**/*.js") + Dir.glob("sample/**/*") +
|
|
29
|
+
["glslkit-webgl.gemspec", "README.md", "CHANGELOG.md", "LICENSE.txt"]
|
|
30
|
+
spec.require_paths = ["lib"]
|
|
31
|
+
|
|
32
|
+
# M12c: `~> 0.1.0` は `Gem::Requirement` の仕様上プレリリース版を一切
|
|
33
|
+
# 満たさない(`"~> 0.1.0".satisfied_by?("0.1.0.pre")` は常にfalse)。
|
|
34
|
+
# 0.1.0.pre のリリース期間中だけこの範囲にし、正式な0.1.0を切ったら
|
|
35
|
+
# `~> 0.1.0` に戻すこと。
|
|
36
|
+
spec.add_dependency "glslkit", ">= 0.1.0.pre", "< 0.2.0"
|
|
37
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
module WebGL
|
|
5
|
+
class Context
|
|
6
|
+
def self.from_selector(selector)
|
|
7
|
+
document = JS.global[:document]
|
|
8
|
+
canvas = document.call(:querySelector, selector)
|
|
9
|
+
if canvas == JS::Null || canvas == JS::Undefined
|
|
10
|
+
raise UnsupportedError, "canvas not found: #{selector}"
|
|
11
|
+
end
|
|
12
|
+
gl = canvas.call(:getContext, "webgl2")
|
|
13
|
+
if gl == JS::Null || gl == JS::Undefined
|
|
14
|
+
raise UnsupportedError, "WebGL2 is not available for #{selector}"
|
|
15
|
+
end
|
|
16
|
+
new(gl, canvas)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# 手書きマニフェスト(M10a前調査5)はglslkitの検証を一切通らずに
|
|
20
|
+
# ここまで来られてしまう。M10eでそのことに一度だけ気付けるようにする。
|
|
21
|
+
# 例外にはしない(手書きマニフェストそのものを禁止する話ではない)。
|
|
22
|
+
# プロセス内で1回だけ出す — Context.debugとは独立の、常時の警告。
|
|
23
|
+
def self.warn_missing_generator_once
|
|
24
|
+
return if @warned_missing_generator
|
|
25
|
+
|
|
26
|
+
@warned_missing_generator = true
|
|
27
|
+
warn "manifest has no generator field; hand-written manifests bypass " \
|
|
28
|
+
"glslkit's validation. Use `rake glslkit:embed`."
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(gl, canvas)
|
|
32
|
+
@gl = gl
|
|
33
|
+
@canvas = canvas
|
|
34
|
+
@performance = JS.global[:performance]
|
|
35
|
+
@request_animation_frame = JS.global
|
|
36
|
+
@state = {program: nil}
|
|
37
|
+
# M11d(reload_program、SPEC-livereload.md §4)専用の追跡状態。
|
|
38
|
+
# development専用の機能であり、リークは許容する
|
|
39
|
+
# (production相当の環境ではlive_reloadをrequireせず、ここが増え
|
|
40
|
+
# 続ける経路自体を使わないため実害が無い。§4.1コメント参照)。
|
|
41
|
+
@programs = {} # name(String) => Program
|
|
42
|
+
@program_names = {}.compare_by_identity # Program => name(String)
|
|
43
|
+
@geometries = Hash.new { |h, k| h[k] = [] } # name(String) => [Geometry]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def width
|
|
47
|
+
@canvas[:width].to_i
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def height
|
|
51
|
+
@canvas[:height].to_i
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def program(manifest, name, vertex:, fragment:, source_maps: {})
|
|
55
|
+
manifest_hash = manifest.respond_to?(:to_h) ? manifest.to_h : manifest
|
|
56
|
+
self.class.warn_missing_generator_once unless manifest_hash.key?("generator")
|
|
57
|
+
entry = manifest_hash.fetch("programs").fetch(name.to_s) do
|
|
58
|
+
raise UnknownProgramError, "program not found in manifest: #{name}"
|
|
59
|
+
end
|
|
60
|
+
normalized_maps = source_maps.each_with_object({}) do |(stage, map), maps|
|
|
61
|
+
maps[stage.to_sym] = map.is_a?(Hash) ? Glslkit::SourceMap.from_h(map) : map
|
|
62
|
+
end
|
|
63
|
+
created = Program.new(@gl, entry,
|
|
64
|
+
vertex: vertex, fragment: fragment, source_maps: normalized_maps, state: @state)
|
|
65
|
+
register_program(name.to_s, created)
|
|
66
|
+
@current_program = created
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 名前で追跡しているProgramを取り出す(ctx.program/ctx.reload_programが
|
|
70
|
+
# 登録したもの)。M11e: reload_program自体はProgramオブジェクトを
|
|
71
|
+
# 返さない(ReloadResultを返す)ため、live_reloadを使う場合はreload後も
|
|
72
|
+
# 「その名前の現在のProgram」をこれで引き直さないと、毎フレームの
|
|
73
|
+
# uniform設定(program.set(:u_time, ...)等)が古い(差し替え済みで
|
|
74
|
+
# 二度と使われない)Programオブジェクトに向いたままになってしまう。
|
|
75
|
+
# 追跡されていない名前ならnil。
|
|
76
|
+
def program_for(name)
|
|
77
|
+
@programs[name.to_s]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def geometry(attributes:, indices: nil, program: nil, mode: nil)
|
|
81
|
+
target = program || @current_program
|
|
82
|
+
raise UnknownProgramError, "create a program before creating geometry" unless target
|
|
83
|
+
|
|
84
|
+
created = Geometry.new(@gl, target, attributes: attributes, indices: indices, mode: mode)
|
|
85
|
+
program_name = @program_names[target]
|
|
86
|
+
@geometries[program_name] << created if program_name
|
|
87
|
+
created
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# M11d: Program単体ではなくContextの責務(SPEC-livereload.md 決定2)。
|
|
91
|
+
# 新しいシェーダのコンパイル・リンクが両方成功して初めて内部の
|
|
92
|
+
# ハンドルを差し替える。失敗時は古いProgramのまま
|
|
93
|
+
# CompileError/LinkErrorをそのまま投げる(§4.1)。
|
|
94
|
+
#
|
|
95
|
+
# attribute locationが新旧で変わっていた場合は、既存のVAOをそのまま
|
|
96
|
+
# 使うと壊れた描画になるため、ReloadIncompatibleErrorを投げて
|
|
97
|
+
# ページのリロードを促す(§4.2)。無言で壊れた描画を続けない。
|
|
98
|
+
#
|
|
99
|
+
# uniform値は名前ベースで引き継ぐ(§4.3)。名前・type・element_countが
|
|
100
|
+
# すべて一致するものだけ復元し、それ以外は破棄してdiagnosticsに
|
|
101
|
+
# 載せる(例外にはしない)。戻り値のResult#ok?はこの診断だけを見る —
|
|
102
|
+
# コンパイル/リンク/location不一致は例外なので、ここまで来た時点で
|
|
103
|
+
# 差し替え自体は成功している。
|
|
104
|
+
def reload_program(name, vertex:, fragment:, manifest:, source_maps: {})
|
|
105
|
+
key = name.to_s
|
|
106
|
+
old_program = @programs.fetch(key) do
|
|
107
|
+
raise UnknownProgramError, "no such program to reload (never created via Context#program): #{name}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
manifest_hash = manifest.respond_to?(:to_h) ? manifest.to_h : manifest
|
|
111
|
+
entry = manifest_hash.fetch("programs").fetch(key) do
|
|
112
|
+
raise UnknownProgramError, "program not found in manifest: #{name}"
|
|
113
|
+
end
|
|
114
|
+
normalized_maps = source_maps.each_with_object({}) do |(stage, map), maps|
|
|
115
|
+
maps[stage.to_sym] = map.is_a?(Hash) ? Glslkit::SourceMap.from_h(map) : map
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
new_program = Program.new(@gl, entry,
|
|
119
|
+
vertex: vertex, fragment: fragment, source_maps: normalized_maps, state: @state)
|
|
120
|
+
|
|
121
|
+
check_geometry_compatibility!(key, new_program)
|
|
122
|
+
|
|
123
|
+
discarded = new_program.restore_uniforms_from(old_program)
|
|
124
|
+
diagnostics = discarded.map { |info| uniform_discard_diagnostic(key, info) }
|
|
125
|
+
|
|
126
|
+
@programs[key] = new_program
|
|
127
|
+
@program_names.delete(old_program)
|
|
128
|
+
@program_names[new_program] = key
|
|
129
|
+
@current_program = new_program if @current_program.equal?(old_program)
|
|
130
|
+
|
|
131
|
+
ReloadResult.new(diagnostics: diagnostics)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def texture2d(width:, height:, data:, unit: 0)
|
|
135
|
+
Texture.new(@gl, width: width, height: height, data: data, unit: unit)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def viewport(width = nil, height = nil, x: 0, y: 0)
|
|
139
|
+
@gl.call(:viewport, x, y, width || self.width, height || self.height)
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def depth_test=(enabled)
|
|
144
|
+
@gl.call(enabled ? :enable : :disable, @gl[:DEPTH_TEST])
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def clear(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0, depth: true)
|
|
148
|
+
@gl.call(:clearColor, red, green, blue, alpha)
|
|
149
|
+
mask = @gl[:COLOR_BUFFER_BIT].to_i
|
|
150
|
+
mask |= @gl[:DEPTH_BUFFER_BIT].to_i if depth
|
|
151
|
+
@gl.call(:clear, mask)
|
|
152
|
+
self
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def draw(geometry, program: nil)
|
|
156
|
+
target = program || @current_program
|
|
157
|
+
raise UnknownProgramError, "no program selected" unless target
|
|
158
|
+
|
|
159
|
+
target.use
|
|
160
|
+
@gl.call(:bindVertexArray, geometry.vao)
|
|
161
|
+
if geometry.indexed
|
|
162
|
+
@gl.call(:drawElements, geometry.mode, geometry.count, geometry.index_type, 0)
|
|
163
|
+
else
|
|
164
|
+
@gl.call(:drawArrays, geometry.mode, 0, geometry.count)
|
|
165
|
+
end
|
|
166
|
+
self
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def loop(&block)
|
|
170
|
+
raise ArgumentError, "block required" unless block
|
|
171
|
+
|
|
172
|
+
start = @performance.call(:now).to_f
|
|
173
|
+
tick = nil
|
|
174
|
+
tick = proc do |timestamp|
|
|
175
|
+
block.call((timestamp.to_f - start) / 1000.0)
|
|
176
|
+
check_error if Glslkit::WebGL.debug
|
|
177
|
+
@request_animation_frame.call(:requestAnimationFrame, tick)
|
|
178
|
+
end
|
|
179
|
+
@request_animation_frame.call(:requestAnimationFrame, tick)
|
|
180
|
+
tick
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def register_program(name, created)
|
|
186
|
+
@programs[name] = created
|
|
187
|
+
@program_names[created] = name
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# このプログラム名で作られた全Geometryについて、構築時に使った
|
|
191
|
+
# attribute locationが新Programでも変わっていないかを確認する。
|
|
192
|
+
# 1つでも変わっていればReloadIncompatibleError。
|
|
193
|
+
def check_geometry_compatibility!(key, new_program)
|
|
194
|
+
@geometries[key].each do |geometry|
|
|
195
|
+
geometry.attribute_locations.each do |attribute_name, old_location|
|
|
196
|
+
new_location = new_program.attribute_locations[attribute_name]
|
|
197
|
+
next if new_location == old_location
|
|
198
|
+
|
|
199
|
+
raise ReloadIncompatibleError,
|
|
200
|
+
"attribute #{attribute_name} location changed (#{old_location.inspect} -> " \
|
|
201
|
+
"#{new_location.inspect}) while reloading #{key}; reload the page"
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def uniform_discard_diagnostic(program_name, discarded)
|
|
207
|
+
message = case discarded.fetch(:reason)
|
|
208
|
+
when :removed
|
|
209
|
+
"uniform #{discarded.fetch(:name)} no longer exists in the reloaded shader; discarding its previous value"
|
|
210
|
+
when :type_changed
|
|
211
|
+
"uniform #{discarded.fetch(:name)} changed type (#{discarded.fetch(:from)} -> " \
|
|
212
|
+
"#{discarded.fetch(:to)}); discarding its previous value"
|
|
213
|
+
end
|
|
214
|
+
Glslkit::Diagnostic.new(
|
|
215
|
+
severity: :warning, code: "W101", message: message,
|
|
216
|
+
program: program_name, stage: nil, name: discarded.fetch(:name).to_s, file: nil, line: nil
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def check_error
|
|
221
|
+
error = @gl.call(:getError).to_i
|
|
222
|
+
return if error == @gl[:NO_ERROR].to_i
|
|
223
|
+
|
|
224
|
+
raise Error, "WebGL error: #{error}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# 開発機(通常のCRuby)で実行するホスト側の生成ツール(M10a)。
|
|
4
|
+
# File/Dir、Preprocessor/Reflection/Validator/digestなどcoreのフル機能を使う。
|
|
5
|
+
# **`glslkit/webgl`(ブラウザ側ランタイムの入口)からは絶対にrequireしないこと。
|
|
6
|
+
# ruby.wasm上では実行しない。** `rake glslkit:embed` からのみ使う想定。
|
|
7
|
+
require "glslkit"
|
|
8
|
+
require "pp"
|
|
9
|
+
require "fileutils"
|
|
10
|
+
|
|
11
|
+
module Glslkit
|
|
12
|
+
module WebGL
|
|
13
|
+
module Embed
|
|
14
|
+
# 冪等性のための固定値(実際の生成時刻ではない)。generated_atが実行の
|
|
15
|
+
# たびに変わるとgit diffがノイズになるため、実時刻の代わりにこれを使う。
|
|
16
|
+
EMBEDDED_TIMESTAMP = Time.at(0).utc
|
|
17
|
+
|
|
18
|
+
class ValidationError < StandardError; end
|
|
19
|
+
class InvalidProgramNameError < StandardError; end
|
|
20
|
+
|
|
21
|
+
# 有効なRubyの定数名(1つの識別子として)であることを要求する。
|
|
22
|
+
MODULE_NAME_PATTERN = /\A[A-Z][A-Za-z0-9_]*\z/
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# shaders_dir配下の.vert/.fragペアごとに1つの.rbファイルをout_dirへ書き出す。
|
|
27
|
+
# 戻り値は [{name:, path:, warnings: [Glslkit::Diagnostic]}, ...]。
|
|
28
|
+
# Validatorでエラーがあれば書き出さず ValidationError を投げる
|
|
29
|
+
# (SPEC.md §8.6: Validator#validate は必ず Manifest.build より前)。
|
|
30
|
+
def generate(shaders_dir:, out_dir:)
|
|
31
|
+
root = File.expand_path(shaders_dir)
|
|
32
|
+
FileUtils.mkdir_p(out_dir)
|
|
33
|
+
resolver = Glslkit::Resolvers::FileSystem.new(load_paths: [root])
|
|
34
|
+
|
|
35
|
+
program_stage_paths(root).sort.map do |name, stages|
|
|
36
|
+
write_program(resolver, name, stages, out_dir)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# 実体(前処理→Program化→検証→Manifest組み立て)は Glslkit::Bundle が
|
|
41
|
+
# 持つ(M11b)。ここは Bundle の結果を見て、生成物として書き出すか
|
|
42
|
+
# 例外を投げるかを判断するだけの薄いラッパー。
|
|
43
|
+
def write_program(resolver, name, stages, out_dir)
|
|
44
|
+
module_name_for(name) # 実際にBundleを呼ぶ前に名前だけ先に検証する(fail fast)。
|
|
45
|
+
result = Glslkit::Bundle.build(
|
|
46
|
+
resolver: resolver, name: name, vertex: stages.fetch(:vertex), fragment: stages.fetch(:fragment),
|
|
47
|
+
line_directives: false, now: EMBEDDED_TIMESTAMP
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
case result.kind
|
|
51
|
+
when :preprocess
|
|
52
|
+
raise result.error
|
|
53
|
+
when :validation
|
|
54
|
+
errors = result.diagnostics.select(&:error?)
|
|
55
|
+
raise ValidationError, "glslkit:embed validation failed for #{name}:\n#{errors.join("\n")}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 警告は生成を止めない(エラーと違う)が、握り潰さず必ず標準エラーに出す。
|
|
59
|
+
# rakeタスク経由でなく Embed.generate を直接呼んだ場合でも見えるように、
|
|
60
|
+
# ここ(ライブラリ側)で出す。呼び出し側での二重出力は行わない。
|
|
61
|
+
warnings = result.diagnostics.select(&:warning?)
|
|
62
|
+
warnings.each { |warning| warn "[glslkit:embed] #{name}: #{warning}" }
|
|
63
|
+
|
|
64
|
+
out_path = File.join(out_dir, "#{name}_shaders.rb")
|
|
65
|
+
File.write(out_path, render(name: name, manifest: result.manifest, vertex: result.vertex, fragment: result.fragment))
|
|
66
|
+
{name: name, path: out_path, warnings: warnings}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# {"hello" => {vertex: "hello.vert", fragment: "hello.frag"}, ...} を返す。
|
|
70
|
+
# 同名の相方が無い.vert/.fragはスキップする(rails側のManifestBuilderと同じ扱い)。
|
|
71
|
+
def program_stage_paths(root)
|
|
72
|
+
by_name = Hash.new { |h, k| h[k] = {} }
|
|
73
|
+
Dir.glob("**/*.{vert,frag}", base: root).each do |relative|
|
|
74
|
+
ext = File.extname(relative)
|
|
75
|
+
stage = (ext == ".vert") ? :vertex : :fragment
|
|
76
|
+
by_name[relative.delete_suffix(ext)][stage] = relative
|
|
77
|
+
end
|
|
78
|
+
by_name.select { |_, stages| stages.key?(:vertex) && stages.key?(:fragment) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def render(name:, manifest:, vertex:, fragment:)
|
|
82
|
+
module_name = module_name_for(name)
|
|
83
|
+
vertex_tag = unique_tag(vertex.code, "GLSLKIT_VERTEX_SRC")
|
|
84
|
+
fragment_tag = unique_tag(fragment.code, "GLSLKIT_FRAGMENT_SRC")
|
|
85
|
+
|
|
86
|
+
<<~RUBY
|
|
87
|
+
# frozen_string_literal: true
|
|
88
|
+
|
|
89
|
+
# 自動生成 (rake glslkit:embed)。手で編集しない。
|
|
90
|
+
# 再生成するには: bundle exec rake glslkit:embed[shaders_dir,out_dir]
|
|
91
|
+
#
|
|
92
|
+
# MANIFESTのgenerated_atが1970-01-01なのはバグではない。生成の
|
|
93
|
+
# 冪等性(同じ入力から同じ出力になること)のための固定値であり、
|
|
94
|
+
# 実行時刻を使うと同じ入力でも実行のたびにgit diffが出てしまう。
|
|
95
|
+
# 再現性はstages.*.digest(実データから計算される)が担保している。
|
|
96
|
+
|
|
97
|
+
module #{module_name}
|
|
98
|
+
VERTEX = #{heredoc_literal(vertex.code, vertex_tag, suffix: ".freeze")}
|
|
99
|
+
|
|
100
|
+
FRAGMENT = #{heredoc_literal(fragment.code, fragment_tag, suffix: ".freeze")}
|
|
101
|
+
|
|
102
|
+
MANIFEST = #{pp_literal(manifest)}.freeze
|
|
103
|
+
|
|
104
|
+
SOURCE_MAPS = {
|
|
105
|
+
vertex: #{pp_literal(vertex.source_map.to_h)},
|
|
106
|
+
fragment: #{pp_literal(fragment.source_map.to_h)}
|
|
107
|
+
}.freeze
|
|
108
|
+
end
|
|
109
|
+
RUBY
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# 非squiggly(`<<~`ではなく`<<`)のシングルクォートヒアドキュメントを使う。
|
|
113
|
+
# `<<~`は最小共通インデントを削るため、code側の行のインデントが
|
|
114
|
+
# 想定と揃っていないとsource.codeとバイト一致しなくなる
|
|
115
|
+
# (例: 全行が2スペース下がっているとsource.codeの2スペースが
|
|
116
|
+
# 削られてしまう)。`<<`は一切ディデントしないため常にバイト一致する。
|
|
117
|
+
def heredoc_literal(code, tag, suffix: "")
|
|
118
|
+
"<<'#{tag}'#{suffix}\n#{code.chomp}\n#{tag}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# codeの中に偶然tagと同じ行が現れても壊れないよう、衝突する限りtagをずらす。
|
|
122
|
+
# 冪等性のため決定的な連番だけを使う(object_idやSecureRandom等は不可)。
|
|
123
|
+
def unique_tag(code, base)
|
|
124
|
+
tag = base
|
|
125
|
+
suffix = 2
|
|
126
|
+
while code.lines.any? { |line| line.strip == tag }
|
|
127
|
+
tag = "#{base}_#{suffix}"
|
|
128
|
+
suffix += 1
|
|
129
|
+
end
|
|
130
|
+
tag
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def pp_literal(value)
|
|
134
|
+
PP.pp(value, +"", 80).chomp
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# 規則(修正4): プログラム名を非英数字(`_` `-` `.` 等、任意の連続)で
|
|
138
|
+
# 単語に分割し、各単語の先頭だけ大文字化して連結、末尾に "Shaders" を
|
|
139
|
+
# 付ける。例: "neon" -> "NeonShaders"、"pbr_lighting" -> "PbrLightingShaders"、
|
|
140
|
+
# "common.glsl" -> "CommonGlslShaders"(`.`も区切り文字として扱う)。
|
|
141
|
+
#
|
|
142
|
+
# 結果が有効なRubyの定数名でなければ(例: "2d-post" -> "2dPostShaders" は
|
|
143
|
+
# 数字始まりで無効)、無言で辻褄を合わせようとせず例外を投げる。
|
|
144
|
+
def module_name_for(name)
|
|
145
|
+
words = name.to_s.split(/[^a-zA-Z0-9]+/).reject(&:empty?)
|
|
146
|
+
camelized = words.map { |word| word[0].upcase + word[1..] }.join
|
|
147
|
+
module_name = "#{camelized}Shaders"
|
|
148
|
+
unless MODULE_NAME_PATTERN.match?(module_name)
|
|
149
|
+
raise InvalidProgramNameError,
|
|
150
|
+
"cannot derive a valid Ruby module name from program #{name.inspect} " \
|
|
151
|
+
"(got #{module_name.inspect}). Rename the .vert/.frag pair so the shared " \
|
|
152
|
+
"basename starts with a letter."
|
|
153
|
+
end
|
|
154
|
+
module_name
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
module WebGL
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
class UnsupportedError < Error; end
|
|
7
|
+
class UnknownProgramError < Error; end
|
|
8
|
+
class UnknownUniformError < Error; end
|
|
9
|
+
class UniformLengthError < Error; end
|
|
10
|
+
class ShaderError < Error; end
|
|
11
|
+
# M11d: reload_program(SPEC-livereload.md §4.2)で新旧のattribute location
|
|
12
|
+
# が一致しない場合に投げる。既存のVAOをそのまま使い続けると壊れた描画に
|
|
13
|
+
# なるため、無言で続行せずページのリロードを促す。
|
|
14
|
+
class ReloadIncompatibleError < Error; end
|
|
15
|
+
|
|
16
|
+
class CompileError < ShaderError
|
|
17
|
+
attr_reader :stage, :file, :line, :raw_log
|
|
18
|
+
|
|
19
|
+
def initialize(stage:, raw_log:, file: nil, line: nil, detail: nil)
|
|
20
|
+
@stage = stage
|
|
21
|
+
@file = file
|
|
22
|
+
@line = line
|
|
23
|
+
@raw_log = raw_log.to_s
|
|
24
|
+
message = detail || @raw_log
|
|
25
|
+
message = "#{file}:#{line}: #{message}" if file && line
|
|
26
|
+
super(message)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class LinkError < ShaderError
|
|
31
|
+
attr_reader :raw_log
|
|
32
|
+
|
|
33
|
+
def initialize(raw_log)
|
|
34
|
+
@raw_log = raw_log.to_s
|
|
35
|
+
super(@raw_log)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|