glslkit-rails 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 +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +222 -0
- data/config/routes.rb +11 -0
- data/glslkit-rails.gemspec +50 -0
- data/lib/glslkit/rails/compiler.rb +55 -0
- data/lib/glslkit/rails/helper.rb +69 -0
- data/lib/glslkit/rails/live_reload/engine.rb +28 -0
- data/lib/glslkit/rails/live_reload/known_files.rb +30 -0
- data/lib/glslkit/rails/live_reload/mount.rb +48 -0
- data/lib/glslkit/rails/live_reload/programs_controller.rb +90 -0
- data/lib/glslkit/rails/manifest_builder.rb +96 -0
- data/lib/glslkit/rails/manifest_reader.rb +46 -0
- data/lib/glslkit/rails/railtie.rb +64 -0
- data/lib/glslkit/rails/shader_programs.rb +34 -0
- data/lib/glslkit/rails/version.rb +7 -0
- data/lib/glslkit/rails.rb +23 -0
- data/lib/tasks/glslkit.rake +47 -0
- metadata +125 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d51a2bdc32253c7b684bee4903f3b0153631fa67be1e0cb440d70a51de043c51
|
|
4
|
+
data.tar.gz: 5352b0f62770e5914db0de3bbe4f1e38d6f0dddc6dd835f1e1bd0e713b091b5b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 93af6788fe949b71df49fe5d43a326123808e5d3a6a2621aac6f9c8d9e4ff91075caced01bdff0910dd22e9ee311013623fc4c5827a603cea0acc9328e783025
|
|
7
|
+
data.tar.gz: 4de7818cab7bb89ce2aed399c4b83130f3b43ab7b68f8ce525080135d2823e9c3ac4cafc8b95d828e366884f2f9b2898beae1c471fa5c024d1e44b62412f7458
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
初回リリース (v0.1)。
|
|
6
|
+
|
|
7
|
+
### 追加
|
|
8
|
+
|
|
9
|
+
- `.glsl` / `.vert` / `.frag` をPropshaftのアセットとして登録
|
|
10
|
+
(content_type: `x-shader/x-vertex` / `x-shader/x-fragment` / `text/plain`)
|
|
11
|
+
- `Glslkit::Rails::Compiler` — Propshaft経由で `#include` を解決した内容を配信
|
|
12
|
+
(developmentは毎リクエスト再処理、productionは`assets:precompile`時に1回)
|
|
13
|
+
- `rake glslkit:reflect` と `assets:precompile` フックによる
|
|
14
|
+
`glsl-manifest.json` の自動生成
|
|
15
|
+
- `Glslkit::Rails.manifest` — Ruby側からの読み取り窓口 (productionはメモ化)
|
|
16
|
+
- `glsl_script_tag` / `glsl_manifest_tag` ビューヘルパー
|
|
17
|
+
(`</script>`エスケープ、CSP nonce対応)
|
|
18
|
+
- `config.glslkit.validate` / `fail_on_warning` / `disabled_checks` —
|
|
19
|
+
`Glslkit::Validator` を `ManifestBuilder` に配線。development では検証結果を
|
|
20
|
+
ログに出すだけで失敗させないが、`assets:precompile` は検証エラーがあれば
|
|
21
|
+
`Glslkit::ValidationFailed` を投げてマニフェストを書き出す前に止める。
|
|
22
|
+
`rake glslkit:check` は設定に関わらず常に検証し、終了コードで判定できる
|
|
23
|
+
- development専用のライブリロードエンドポイント(`ActionController::API`
|
|
24
|
+
ベースのEngine、決定7)。`GET /glslkit/digests.json`(ソースdigestのみ)と
|
|
25
|
+
`GET /glslkit/programs/:name.json`(シェーダ本文・マニフェスト・SourceMap
|
|
26
|
+
・診断)。production相当の環境ではミドルウェア自体を挿入しないため一切
|
|
27
|
+
存在しない。`glslkit-webgl`側の`Context#reload_program`/`live_reload`と
|
|
28
|
+
組み合わせて使う(0.1.0の看板機能)
|
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,222 @@
|
|
|
1
|
+
# glslkit-rails
|
|
2
|
+
|
|
3
|
+
[`glslkit`](../core) を Rails (Propshaft) のアセットパイプラインに統合する gem。
|
|
4
|
+
`.glsl` / `.vert` / `.frag` をアセットとして登録し、`#include` を解決した内容を
|
|
5
|
+
配信し、`assets:precompile` 時にリフレクションマニフェストを書き出す。
|
|
6
|
+
|
|
7
|
+
## インストール
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "glslkit-rails"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`glslkit`・`railties >= 7.1`・`propshaft >= 1.3.2` に依存する。
|
|
14
|
+
|
|
15
|
+
## 設定
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# config/application.rb
|
|
19
|
+
config.glslkit.paths = ["app/shaders"] # デフォルト: app/shaders
|
|
20
|
+
config.glslkit.minify = Rails.env.production?
|
|
21
|
+
config.glslkit.line_directives = !Rails.env.production?
|
|
22
|
+
config.glslkit.manifest_path = "glsl-manifest.json"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## アセットとしての配信
|
|
26
|
+
|
|
27
|
+
`config.glslkit.paths` 配下のファイルはPropshaftのload_pathに追加され、
|
|
28
|
+
`#include` を解決した内容がcontent_type `x-shader/x-vertex` /
|
|
29
|
+
`x-shader/x-fragment` / `text/plain` で配信される。developmentでは
|
|
30
|
+
毎リクエスト再処理し、productionでは `assets:precompile` 時に1回だけ処理する。
|
|
31
|
+
|
|
32
|
+
## マニフェスト
|
|
33
|
+
|
|
34
|
+
`rake assets:precompile` を実行すると、自動的に
|
|
35
|
+
`public/assets/<config.glslkit.manifest_path>` (デフォルト `glsl-manifest.json`)
|
|
36
|
+
にリフレクションマニフェストを書き出す。単体で実行する場合:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
rake glslkit:reflect
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Ruby側から読む場合:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
Glslkit::Rails.manifest # => Hash (productionでは読み込み結果をメモ化する)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## ビューヘルパー
|
|
49
|
+
|
|
50
|
+
```erb
|
|
51
|
+
<%= glsl_script_tag "pbr.vert" %>
|
|
52
|
+
<%# => <script type="x-shader/x-vertex" id="pbr-vert">...</script> (インライン埋め込み) %>
|
|
53
|
+
|
|
54
|
+
<%= glsl_manifest_tag %>
|
|
55
|
+
<%# => <script type="application/json" id="glsl-manifest">...</script> %>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- インライン埋め込みの内容は `</script` をエスケープしている
|
|
59
|
+
- そのリクエストでCSPが実際に有効な場合(`content_security_policy?`)、
|
|
60
|
+
自動的に `nonce` 属性を付与する。`nonce: true`/`false` で強制・抑止も
|
|
61
|
+
できる
|
|
62
|
+
|
|
63
|
+
## 実践的な例
|
|
64
|
+
|
|
65
|
+
`app/shaders/` にPBR風のマテリアルシェーダーを置く。共有partialの
|
|
66
|
+
uniform block、複数テクスチャ、uniform配列を含む、より現実的な構成:
|
|
67
|
+
|
|
68
|
+
```glsl
|
|
69
|
+
# app/shaders/common/camera.glsl
|
|
70
|
+
layout(std140, binding = 0) uniform Camera {
|
|
71
|
+
mat4 view;
|
|
72
|
+
mat4 projection;
|
|
73
|
+
vec3 eye_position;
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```glsl
|
|
78
|
+
# app/shaders/material.vert
|
|
79
|
+
#version 300 es
|
|
80
|
+
#include "common/camera.glsl"
|
|
81
|
+
|
|
82
|
+
layout(location = 0) in vec3 a_position;
|
|
83
|
+
layout(location = 1) in vec3 a_normal;
|
|
84
|
+
layout(location = 2) in vec2 a_uv;
|
|
85
|
+
|
|
86
|
+
uniform mat4 u_model;
|
|
87
|
+
|
|
88
|
+
out vec3 v_world_position;
|
|
89
|
+
out vec3 v_normal;
|
|
90
|
+
out vec2 v_uv;
|
|
91
|
+
|
|
92
|
+
void main() {
|
|
93
|
+
vec4 world_position = u_model * vec4(a_position, 1.0);
|
|
94
|
+
v_world_position = world_position.xyz;
|
|
95
|
+
v_normal = mat3(u_model) * a_normal;
|
|
96
|
+
v_uv = a_uv;
|
|
97
|
+
gl_Position = projection * view * world_position;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```glsl
|
|
102
|
+
# app/shaders/material.frag
|
|
103
|
+
#version 300 es
|
|
104
|
+
precision highp float;
|
|
105
|
+
#include "common/camera.glsl"
|
|
106
|
+
|
|
107
|
+
in vec3 v_world_position;
|
|
108
|
+
in vec3 v_normal;
|
|
109
|
+
in vec2 v_uv;
|
|
110
|
+
|
|
111
|
+
uniform sampler2D u_albedo_map;
|
|
112
|
+
uniform vec4 u_base_color;
|
|
113
|
+
uniform vec3 u_light_positions[4];
|
|
114
|
+
uniform vec3 u_light_colors[4];
|
|
115
|
+
uniform int u_light_count;
|
|
116
|
+
|
|
117
|
+
out vec4 fragColor;
|
|
118
|
+
|
|
119
|
+
void main() {
|
|
120
|
+
vec3 albedo = texture(u_albedo_map, v_uv).rgb * u_base_color.rgb;
|
|
121
|
+
vec3 n = normalize(v_normal);
|
|
122
|
+
|
|
123
|
+
vec3 total = vec3(0.0);
|
|
124
|
+
for (int i = 0; i < u_light_count; i++) {
|
|
125
|
+
vec3 to_light = normalize(u_light_positions[i] - v_world_position);
|
|
126
|
+
total += albedo * u_light_colors[i] * max(dot(n, to_light), 0.0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fragColor = vec4(total, u_base_color.a);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
ビューでは平坦化済みシェーダーとマニフェストをまとめて埋め込む:
|
|
134
|
+
|
|
135
|
+
```erb
|
|
136
|
+
<%# app/views/scenes/show.html.erb %>
|
|
137
|
+
<%= glsl_script_tag "material.vert" %>
|
|
138
|
+
<%= glsl_script_tag "material.frag" %>
|
|
139
|
+
<%= glsl_manifest_tag %>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`glsl_script_tag "material.vert"` は `#include "common/camera.glsl"` を
|
|
143
|
+
解決した平坦化済みGLSLを `id="material-vert"` として埋め込み、
|
|
144
|
+
`glsl_manifest_tag` は attribute の location・uniform の型/setter/配列サイズ・
|
|
145
|
+
uniform blockのlayout/bindingを持つJSONを `id="glsl-manifest"` として埋め込む。
|
|
146
|
+
|
|
147
|
+
JS側は、このマニフェストを消費するだけでWebGLプログラムをセットアップできる。
|
|
148
|
+
**`gl.getActiveUniform`/`gl.getActiveAttrib` でイントロスペクションするループが
|
|
149
|
+
不要になる**のがglslkitの狙い:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
function compileProgram(gl, manifestProgram, shaderElementIds) {
|
|
153
|
+
const vertexShader = compileShader(gl, gl.VERTEX_SHADER,
|
|
154
|
+
document.getElementById(shaderElementIds.vertex).textContent);
|
|
155
|
+
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER,
|
|
156
|
+
document.getElementById(shaderElementIds.fragment).textContent);
|
|
157
|
+
|
|
158
|
+
const program = gl.createProgram();
|
|
159
|
+
gl.attachShader(program, vertexShader);
|
|
160
|
+
gl.attachShader(program, fragmentShader);
|
|
161
|
+
gl.linkProgram(program);
|
|
162
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
163
|
+
throw new Error(gl.getProgramInfoLog(program));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const attributes = {};
|
|
167
|
+
for (const attr of manifestProgram.attributes) {
|
|
168
|
+
// location: null の場合だけ getAttribLocation を引けばよい
|
|
169
|
+
attributes[attr.name] = attr.location ?? gl.getAttribLocation(program, attr.name);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const uniforms = {};
|
|
173
|
+
let textureUnit = 0;
|
|
174
|
+
for (const uniform of manifestProgram.uniforms) {
|
|
175
|
+
const location = gl.getUniformLocation(program, uniform.name);
|
|
176
|
+
if (!location) continue; // 未使用uniformはコンパイラに削除されうる
|
|
177
|
+
|
|
178
|
+
if (uniform.sampler) {
|
|
179
|
+
const unit = textureUnit++;
|
|
180
|
+
uniforms[uniform.name] = (texture) => {
|
|
181
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
182
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
183
|
+
gl.uniform1i(location, unit);
|
|
184
|
+
};
|
|
185
|
+
} else if (uniform.matrix) {
|
|
186
|
+
uniforms[uniform.name] = (value) => gl[uniform.setter](location, false, value);
|
|
187
|
+
} else {
|
|
188
|
+
uniforms[uniform.name] = (value) => gl[uniform.setter](location, value);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {program, attributes, uniforms};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function compileShader(gl, type, source) {
|
|
196
|
+
const shader = gl.createShader(type);
|
|
197
|
+
gl.shaderSource(shader, source);
|
|
198
|
+
gl.compileShader(shader);
|
|
199
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
200
|
+
throw new Error(gl.getShaderInfoLog(shader));
|
|
201
|
+
}
|
|
202
|
+
return shader;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const manifest = JSON.parse(document.getElementById("glsl-manifest").textContent);
|
|
206
|
+
const material = compileProgram(gl, manifest.programs.material, {
|
|
207
|
+
vertex: "material-vert",
|
|
208
|
+
fragment: "material-frag",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
material.uniforms.u_model(modelMatrix);
|
|
212
|
+
material.uniforms.u_base_color(new Float32Array([1, 1, 1, 1]));
|
|
213
|
+
material.uniforms.u_albedo_map(albedoTexture);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`uniform.setter`/`uniform.matrix`/`uniform.sampler`はすべて
|
|
217
|
+
[`Glslkit::Types`](../core#マニフェスト生成)由来で、`spec/schema/reflection-v1.json`
|
|
218
|
+
に準拠する。
|
|
219
|
+
|
|
220
|
+
## ライセンス
|
|
221
|
+
|
|
222
|
+
[MIT](LICENSE.txt)
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Glslkit::Rails::LiveReload::Engine 自身のルート(M11c、SPEC-livereload.md §3.3)。
|
|
4
|
+
# このファイルは `Rails::Engine.root` が glslkit-rails 自体になるため、
|
|
5
|
+
# ホストアプリの config/routes.rb とは無関係。ホストアプリへの接続は
|
|
6
|
+
# glslkit-rails/lib/glslkit/rails/live_reload/mount.rb がミドルウェアとして
|
|
7
|
+
# 行う(理由はそちらのコメントを参照)。
|
|
8
|
+
Glslkit::Rails::LiveReload::Engine.routes.draw do
|
|
9
|
+
get "digests.json", to: "programs#digests"
|
|
10
|
+
get "programs/:name", to: "programs#show"
|
|
11
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/glslkit/rails/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "glslkit-rails"
|
|
7
|
+
spec.version = Glslkit::Rails::VERSION
|
|
8
|
+
spec.authors = ["kyubey1228"]
|
|
9
|
+
spec.email = ["kyuuka1228@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Rails (Propshaft) integration for glslkit"
|
|
12
|
+
spec.description = "Registers .glsl/.vert/.frag assets with Propshaft, resolves " \
|
|
13
|
+
"#include directives at request/precompile time, and publishes a shader " \
|
|
14
|
+
"reflection manifest for consumption by views and JS at runtime."
|
|
15
|
+
spec.homepage = "https://github.com/kyubey1228/glslkit"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
spec.required_ruby_version = ">= 3.1"
|
|
18
|
+
|
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
|
+
spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/main/rails"
|
|
21
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/rails/CHANGELOG.md"
|
|
22
|
+
|
|
23
|
+
spec.files = Dir.glob("lib/**/*.{rb,rake}") + Dir.glob("config/**/*.rb") +
|
|
24
|
+
["glslkit-rails.gemspec", "README.md", "CHANGELOG.md", "LICENSE.txt"]
|
|
25
|
+
spec.require_paths = ["lib"]
|
|
26
|
+
|
|
27
|
+
# M12c: `~> 0.1.0` は `Gem::Requirement` の仕様上プレリリース版を一切
|
|
28
|
+
# 満たさない(`"~> 0.1.0".satisfied_by?("0.1.0.pre")` は常にfalse)。
|
|
29
|
+
# 0.1.0.pre のリリース期間中だけこの範囲にし、正式な0.1.0を切ったら
|
|
30
|
+
# `~> 0.1.0` に戻すこと。
|
|
31
|
+
spec.add_dependency "glslkit", ">= 0.1.0.pre", "< 0.2.0"
|
|
32
|
+
# M12b: `gem build`は無制限の">= 7.1"を非推奨としてwarnする。上限を
|
|
33
|
+
# 付ける案(CIで検証済みの範囲に基づき`< 9`等)も検討したが、採用しない。
|
|
34
|
+
# 上限は「まだ検証していない将来のメジャーバージョン」を無条件で締め出す
|
|
35
|
+
# ため、Rails側が実際には互換性を保ったままリリースしても、こちらが
|
|
36
|
+
# gemを更新するまで利用者は新しいRailsに上げられなくなる。CIで検証
|
|
37
|
+
# しているのはRuby 3.1→Rails 7.2、既定→Rails 8.1の範囲だが、それを
|
|
38
|
+
# 「動作保証の上限」と誤解させたくない。`gem build`の警告は許容する。
|
|
39
|
+
spec.add_dependency "railties", ">= 7.1"
|
|
40
|
+
# railties >= 7.1に推移的に含まれるため追加インストールは発生しないが、
|
|
41
|
+
# M11でActionController::APIベースのライブリロードEngineを提供するため
|
|
42
|
+
# 明示的に宣言する(SPEC-livereload.md 決定7)。
|
|
43
|
+
spec.add_dependency "actionpack", ">= 7.1"
|
|
44
|
+
# 実際にインストールされた1.3.2で確認したCompiler API
|
|
45
|
+
# (Propshaft::Compiler#compile(asset, input), #referenced_by(asset),
|
|
46
|
+
# Propshaft::Compilers#register(mime_type, klass)) に合わせて固定している。
|
|
47
|
+
# それより古い1.x系では確認していないため、より広い範囲は主張しない。
|
|
48
|
+
# 上限を付けない理由はrailties/actionpackと同じ。
|
|
49
|
+
spec.add_dependency "propshaft", ">= 1.3.2"
|
|
50
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require "propshaft/compiler"
|
|
5
|
+
|
|
6
|
+
module Glslkit
|
|
7
|
+
module Rails
|
|
8
|
+
# x-shader/x-vertex, x-shader/x-fragment, text/plain (§4.1) の3つに
|
|
9
|
+
# 登録される。前者2つは我々専用だが、text/plainはPropshaftが配信しうる
|
|
10
|
+
# 他のあらゆるプレーンテキストアセットと共有される — Propshaftはcompiler
|
|
11
|
+
# の登録をcontent_typeで紐付ける(拡張子ではない)ため、これが無いと
|
|
12
|
+
# このcompilerが無関係なtext/plainアセットにまで作用してしまう。
|
|
13
|
+
# 以下の拡張子ガードがそれを防いでいる。
|
|
14
|
+
class Compiler < ::Propshaft::Compiler
|
|
15
|
+
RELEVANT_EXTENSIONS = %w[.glsl .vert .frag].freeze
|
|
16
|
+
|
|
17
|
+
def compile(asset, input)
|
|
18
|
+
return input unless relevant?(asset)
|
|
19
|
+
|
|
20
|
+
source = preprocess(asset)
|
|
21
|
+
minify? ? Glslkit::Minifier.minify(source.code) : source.code
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def referenced_by(asset)
|
|
25
|
+
return Set.new unless relevant?(asset)
|
|
26
|
+
|
|
27
|
+
source = preprocess(asset)
|
|
28
|
+
source.source_map.files.drop(1).filter_map { |path| load_path.find(path) }.to_set
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def relevant?(asset)
|
|
34
|
+
RELEVANT_EXTENSIONS.include?(asset.logical_path.extname)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def preprocess(asset)
|
|
38
|
+
resolver = Glslkit::Resolvers::FileSystem.new(load_paths: load_path.paths.map(&:to_s))
|
|
39
|
+
Glslkit::Preprocessor.new(resolver: resolver, line_directives: line_directives?).process(asset.logical_path.to_s)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def glslkit_config
|
|
43
|
+
::Rails.application.config.glslkit
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def minify?
|
|
47
|
+
glslkit_config.minify
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def line_directives?
|
|
51
|
+
glslkit_config.line_directives
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
module Rails
|
|
5
|
+
# ActionView上でincludeされるビューヘルパー (§4.3)。
|
|
6
|
+
module Helper
|
|
7
|
+
# <%= glsl_script_tag "pbr.vert" %>
|
|
8
|
+
# => <script type="x-shader/x-vertex" id="pbr-vert">...</script>
|
|
9
|
+
def glsl_script_tag(logical_path, **html_options)
|
|
10
|
+
asset = find_glsl_asset(logical_path)
|
|
11
|
+
html_options[:id] ||= glsl_script_id(logical_path.to_s)
|
|
12
|
+
html_options[:type] = asset.content_type.to_s
|
|
13
|
+
apply_glsl_nonce!(html_options)
|
|
14
|
+
|
|
15
|
+
content_tag(:script, escape_closing_script_tags(asset.compiled_content).html_safe, html_options)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# <%= glsl_manifest_tag %>
|
|
19
|
+
# => <script type="application/json" id="glsl-manifest">...</script>
|
|
20
|
+
def glsl_manifest_tag(**html_options)
|
|
21
|
+
html_options[:id] ||= "glsl-manifest"
|
|
22
|
+
html_options[:type] = "application/json"
|
|
23
|
+
apply_glsl_nonce!(html_options)
|
|
24
|
+
|
|
25
|
+
content_tag(:script, escape_closing_script_tags(Glslkit::Rails.manifest.to_json).html_safe, html_options)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def find_glsl_asset(logical_path)
|
|
31
|
+
::Rails.application.assets.load_path.find(logical_path.to_s) ||
|
|
32
|
+
raise(ArgumentError, "no such glslkit asset: #{logical_path.inspect}")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# "pbr.vert" => "pbr-vert", "effects/glow.frag" => "effects-glow-frag"
|
|
36
|
+
def glsl_script_id(logical_path)
|
|
37
|
+
base = logical_path.sub(/\.(vert|frag|glsl)\z/, "")
|
|
38
|
+
"#{base.tr("/", "-")}-#{Regexp.last_match(1)}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Rails自身のjavascript_tagがJS_ESCAPE_MAPで"</" => '<\/'として行っている
|
|
42
|
+
# のと同じ理由・同じエスケープ: script要素の中身に文字通り"</"が現れると、
|
|
43
|
+
# 中身がJS/JSONとして妥当かどうかに関わらずHTMLパーサがそこでタグを
|
|
44
|
+
# 閉じてしまう。
|
|
45
|
+
def escape_closing_script_tags(content)
|
|
46
|
+
content.gsub("</", '<\/')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# true=強制、false=抑止、未指定ならこのリクエストでCSPが実際に有効な
|
|
50
|
+
# 場合だけ自動付与する。
|
|
51
|
+
#
|
|
52
|
+
# 当初は javascript_tag と全く同じ規約
|
|
53
|
+
# (ActionView::Helpers::JavaScriptHelper.auto_include_nonce)に合わせて
|
|
54
|
+
# いたが、これは config.content_security_policy_nonce_auto という
|
|
55
|
+
# Rails 8.1で新規追加された設定に依存しており、railties >= 7.1という
|
|
56
|
+
# このgemの依存下限では単純にメソッドが存在せずNoMethodErrorになる
|
|
57
|
+
# (CIでRuby 3.1 → Rails 7.2に解決された際に発覚)。content_security_policy?
|
|
58
|
+
# はRails 7.1時点から存在するので、こちらを使う。
|
|
59
|
+
def apply_glsl_nonce!(html_options)
|
|
60
|
+
if html_options[:nonce] == false
|
|
61
|
+
html_options.delete(:nonce)
|
|
62
|
+
elsif html_options[:nonce] == true ||
|
|
63
|
+
(!html_options.key?(:nonce) && respond_to?(:content_security_policy?) && content_security_policy?)
|
|
64
|
+
html_options[:nonce] = content_security_policy_nonce
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
|
|
5
|
+
module Glslkit
|
|
6
|
+
module Rails
|
|
7
|
+
module LiveReload
|
|
8
|
+
# ライブリロード用の2エンドポイント(§3.3)を提供するEngine。
|
|
9
|
+
# `isolate_namespace` によりホストアプリの `ProgramsController` 等と
|
|
10
|
+
# 衝突しない。マウントは `Railtie` が `config.glslkit.live_reload` を
|
|
11
|
+
# 見て判断する — このEngineが定義されていること自体は production でも
|
|
12
|
+
# 構わない(未マウントならルートは一切生成されない)。
|
|
13
|
+
#
|
|
14
|
+
# ルートは `rails/config/routes.rb`(このEngineの root 直下)に書いて
|
|
15
|
+
# いる。クラス本体で直接 `routes.draw do ... end` する形も動くが、
|
|
16
|
+
# それは一度きりの実行にしかならない。Rails::Engineは自身のroute_setを
|
|
17
|
+
# `app.routes_reloader.route_sets` に登録し、アプリのルートリロード
|
|
18
|
+
# サイクルで一緒に clear!/reload される — Rails 7.2ではこの
|
|
19
|
+
# サイクルで実際に一度 clear! されるため、クラス本体に書いた内容は
|
|
20
|
+
# 再度描画されず空になって消える(Rails 8.1では起きなかったため
|
|
21
|
+
# 実機検証で気づくまで見逃していた)。`config/routes.rb` ファイルに
|
|
22
|
+
# すればreload時に毎回 `load` され直すため、バージョンに関わらず安定する。
|
|
23
|
+
class Engine < ::Rails::Engine
|
|
24
|
+
isolate_namespace Glslkit::Rails::LiveReload
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
module Rails
|
|
5
|
+
module LiveReload
|
|
6
|
+
# プログラム名ごとに「最後に成功したビルドの files」をプロセス内メモリで
|
|
7
|
+
# 保持する。Glslkit::Bundle.build の `known_files:` に渡すためのもので、
|
|
8
|
+
# 前処理が失敗した回でも意味のある source_digest を計算できるようにする
|
|
9
|
+
# (SPEC-livereload.md §3.2 決定1)。development専用の機能なので永続化は
|
|
10
|
+
# 不要(プロセスを跨いだ保持は求めない)。
|
|
11
|
+
#
|
|
12
|
+
# development でも Puma は複数スレッドで動くため、単純な Hash への
|
|
13
|
+
# 読み書きではレースの余地がある。Mutex で保護する。
|
|
14
|
+
module KnownFiles
|
|
15
|
+
MUTEX = Mutex.new
|
|
16
|
+
@files_by_name = {}
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def fetch(name)
|
|
21
|
+
MUTEX.synchronize { @files_by_name.fetch(name, []) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def remember(name, files)
|
|
25
|
+
MUTEX.synchronize { @files_by_name[name] = files }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
module Rails
|
|
5
|
+
module LiveReload
|
|
6
|
+
# `/glslkit/*` へのリクエストをEngineへ振り分ける薄いミドルウェア。
|
|
7
|
+
#
|
|
8
|
+
# 当初は `app.routes.append { mount Engine => "/glslkit" }` を
|
|
9
|
+
# Railtieのinitializerから呼ぶ形にしていたが、これは
|
|
10
|
+
# `ActionDispatch::Routing::RouteSet#finalize!`(`@append`ブロックの
|
|
11
|
+
# 反映はここでしか起きない)が、そのinitializerより先に一度でも
|
|
12
|
+
# 走っているかどうかに依存してしまい、Railtie初期化順序次第で
|
|
13
|
+
# 再現性なく404になることが実機検証で判明した(finalize!後に
|
|
14
|
+
# appendしても再finalizeされない)。
|
|
15
|
+
#
|
|
16
|
+
# ミドルウェアスタックは全initializerが完了した後に一度だけ構築される
|
|
17
|
+
# ため、`config.middleware.use`によるこの登録はタイミング非依存で
|
|
18
|
+
# 安定して動く。Engine自体は本物のRails::Engineであり、内部の
|
|
19
|
+
# ルーティング(digests.json / programs/:name)は変更していない。
|
|
20
|
+
class Mount
|
|
21
|
+
PREFIX = "/glslkit"
|
|
22
|
+
|
|
23
|
+
def initialize(app, engine)
|
|
24
|
+
@app = app
|
|
25
|
+
@engine = engine
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call(env)
|
|
29
|
+
path = env["PATH_INFO"]
|
|
30
|
+
return @app.call(env) unless path == PREFIX || path.start_with?("#{PREFIX}/")
|
|
31
|
+
|
|
32
|
+
# 通常の `mount` がやるのと同じ書き換え(SCRIPT_NAME/PATH_INFOの
|
|
33
|
+
# 付け替え)を自分で行う。Engine自身のルート(`digests.json` /
|
|
34
|
+
# `programs/:name`)はプレフィックス無しのパスとして定義している
|
|
35
|
+
# ため、これをしないとEngine内のルーティングが一致しない。
|
|
36
|
+
rest = path.delete_prefix(PREFIX)
|
|
37
|
+
rest = "/#{rest}".squeeze("/")
|
|
38
|
+
|
|
39
|
+
env = env.merge(
|
|
40
|
+
"SCRIPT_NAME" => "#{env["SCRIPT_NAME"]}#{PREFIX}",
|
|
41
|
+
"PATH_INFO" => rest
|
|
42
|
+
)
|
|
43
|
+
@engine.call(env)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_controller/api"
|
|
4
|
+
|
|
5
|
+
module Glslkit
|
|
6
|
+
module Rails
|
|
7
|
+
module LiveReload
|
|
8
|
+
# ライブリロードのポーリング対象になる2つのエンドポイント(§3.3)。
|
|
9
|
+
# ActionController::API を使う(SPEC-livereload.md 決定7)。development
|
|
10
|
+
# でのみマウントされる(Engine#config.glslkit.live_reload が false の
|
|
11
|
+
# 場合は Railtie が一切マウントしないため、production ではこの
|
|
12
|
+
# コントローラ自体にルーティングで到達できない)。
|
|
13
|
+
#
|
|
14
|
+
# Preprocessor/Validator の失敗は200 + ペイロードで返す(§3.4)。それ以外の
|
|
15
|
+
# 想定外の例外はここで握り潰さず、素通しして500にする。
|
|
16
|
+
class ProgramsController < ActionController::API
|
|
17
|
+
# GET /glslkit/digests.json => {"neon" => "5059f642...", ...}
|
|
18
|
+
# ソースdigest(決定1)のみを返す軽量エンドポイント。
|
|
19
|
+
def digests
|
|
20
|
+
digests = ShaderPrograms.discover(rails_app).each_with_object({}) do |(name, stages), acc|
|
|
21
|
+
acc[name] = build(name, stages).source_digest
|
|
22
|
+
end
|
|
23
|
+
render json: digests
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# GET /glslkit/programs/:name.json => §3.2 のJSON(失敗時は§3.4)。
|
|
27
|
+
def show
|
|
28
|
+
programs = ShaderPrograms.discover(rails_app)
|
|
29
|
+
stages = programs[params[:name]]
|
|
30
|
+
return head :not_found unless stages
|
|
31
|
+
|
|
32
|
+
render json: payload_for(build(params[:name], stages))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def rails_app
|
|
38
|
+
::Rails.application
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def glslkit_config
|
|
42
|
+
rails_app.config.glslkit
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build(name, stages)
|
|
46
|
+
resolver = Glslkit::Resolvers::FileSystem.new(load_paths: ShaderPrograms.roots(rails_app))
|
|
47
|
+
result = Glslkit::Bundle.build(
|
|
48
|
+
resolver: resolver, name: name, vertex: stages.fetch(:vertex), fragment: stages.fetch(:fragment),
|
|
49
|
+
line_directives: glslkit_config.line_directives, disabled_checks: glslkit_config.disabled_checks,
|
|
50
|
+
known_files: KnownFiles.fetch(name)
|
|
51
|
+
)
|
|
52
|
+
KnownFiles.remember(name, (result.vertex.source_map.files + result.fragment.source_map.files).uniq) if result.ok?
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def payload_for(result)
|
|
57
|
+
return success_payload(result) if result.ok?
|
|
58
|
+
|
|
59
|
+
error_payload(result)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def success_payload(result)
|
|
63
|
+
{
|
|
64
|
+
name: result.name,
|
|
65
|
+
source_digest: result.source_digest,
|
|
66
|
+
vertex: result.vertex.code,
|
|
67
|
+
fragment: result.fragment.code,
|
|
68
|
+
manifest: result.manifest,
|
|
69
|
+
source_maps: {vertex: result.vertex.source_map.to_h, fragment: result.fragment.source_map.to_h},
|
|
70
|
+
diagnostics: result.diagnostics.map { |d| diagnostic_hash(d) }
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def error_payload(result)
|
|
75
|
+
error = (result.kind == :validation) ? {diagnostics: result.diagnostics.map { |d| diagnostic_hash(d) }} : {message: result.error.message, class: result.error.class.name}
|
|
76
|
+
|
|
77
|
+
{name: result.name, source_digest: result.source_digest, error: {kind: result.kind.to_s, **error}}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def diagnostic_hash(diagnostic)
|
|
81
|
+
{
|
|
82
|
+
severity: diagnostic.severity, code: diagnostic.code, message: diagnostic.message,
|
|
83
|
+
program: diagnostic.program, stage: diagnostic.stage, name: diagnostic.name,
|
|
84
|
+
file: diagnostic.file, line: diagnostic.line
|
|
85
|
+
}.compact
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Glslkit
|
|
6
|
+
module Rails
|
|
7
|
+
# config.glslkit.paths 配下からvertex/fragmentのペアを検出し、その
|
|
8
|
+
# すべてについて reflection-v1.json のマニフェスト(Glslkit::Manifest, M4)
|
|
9
|
+
# を組み立てる。stage.urlは Rails.application.assets.resolver 経由で
|
|
10
|
+
# 解決する。これはasset_path/asset_urlが内部で使っているのとまさに同じ
|
|
11
|
+
# 経路であり、developmentではDynamic、assets:precompile実行後はStatic
|
|
12
|
+
# (Propshaftが直前に書いた.manifest.jsonを読む)になる。
|
|
13
|
+
class ManifestBuilder
|
|
14
|
+
attr_reader :diagnostics
|
|
15
|
+
|
|
16
|
+
def initialize(app)
|
|
17
|
+
@app = app
|
|
18
|
+
@diagnostics = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Validatorは常に実行し、@diagnosticsに集める(config.glslkit.validateの
|
|
22
|
+
# 値によらない)。`config.glslkit.validate`は「診断を集めるかどうか」
|
|
23
|
+
# ではなく「その診断を理由にビルド/リクエストを失敗させるかどうか」を
|
|
24
|
+
# 制御する。呼び出し側(Glslkit::Rails.manifest / rake glslkit:reflect /
|
|
25
|
+
# rake glslkit:check)がそれぞれの文脈でdiagnostics/failing?を見て
|
|
26
|
+
# 反応を決める(SPEC.md §8.6)。
|
|
27
|
+
def build
|
|
28
|
+
@diagnostics = []
|
|
29
|
+
manifest = Glslkit::Manifest.new(generated_at: Time.now.utc.iso8601)
|
|
30
|
+
programs.each do |name, stages|
|
|
31
|
+
vertex_source = preprocessor.process(stages.fetch(:vertex))
|
|
32
|
+
fragment_source = preprocessor.process(stages.fetch(:fragment))
|
|
33
|
+
validate(name, vertex_source, fragment_source)
|
|
34
|
+
|
|
35
|
+
manifest.add_program(
|
|
36
|
+
name,
|
|
37
|
+
vertex: {path: stages.fetch(:vertex), source: vertex_source, url: asset_url(stages.fetch(:vertex))},
|
|
38
|
+
fragment: {path: stages.fetch(:fragment), source: fragment_source, url: asset_url(stages.fetch(:fragment))}
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
manifest.to_h
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# エラーが1件でもあれば真。`fail_on_warning`が有効なら警告のみでも真。
|
|
45
|
+
# config.glslkit.validateがfalseなら常に偽(検証結果があっても
|
|
46
|
+
# ビルド/リクエストを失敗させない)。
|
|
47
|
+
def failing?
|
|
48
|
+
return false unless glslkit_config.validate
|
|
49
|
+
|
|
50
|
+
return true if @diagnostics.any?(&:error?)
|
|
51
|
+
|
|
52
|
+
glslkit_config.fail_on_warning && @diagnostics.any?(&:warning?)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Validator#validate は必ず Manifest.build(ここでは manifest.add_program)
|
|
58
|
+
# より前に呼ぶこと(SPEC.md §8.4/§8.6)。同一ステージ内の重複などは
|
|
59
|
+
# マージ後には検出できないため。
|
|
60
|
+
def validate(name, vertex_source, fragment_source)
|
|
61
|
+
program = Glslkit::Program.new(name: name, sources: {vertex: vertex_source, fragment: fragment_source})
|
|
62
|
+
result = Glslkit::Validator.new(disabled: glslkit_config.disabled_checks).validate(program)
|
|
63
|
+
@diagnostics.concat(result.diagnostics)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def glslkit_config
|
|
67
|
+
@app.config.glslkit
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 発見ロジック本体はShaderProgramsに集約している(LiveReload::ProgramsController
|
|
71
|
+
# と共有するため、M11c)。
|
|
72
|
+
def shader_roots
|
|
73
|
+
ShaderPrograms.roots(@app)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def programs
|
|
77
|
+
ShaderPrograms.discover(@app)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def stage_input(relative_path)
|
|
81
|
+
{path: relative_path, source: preprocessor.process(relative_path), url: asset_url(relative_path)}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def preprocessor
|
|
85
|
+
@preprocessor ||= Glslkit::Preprocessor.new(
|
|
86
|
+
resolver: Glslkit::Resolvers::FileSystem.new(load_paths: shader_roots),
|
|
87
|
+
line_directives: glslkit_config.line_directives
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def asset_url(relative_path)
|
|
92
|
+
@app.assets.resolver.resolve(relative_path)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Glslkit
|
|
6
|
+
module Rails
|
|
7
|
+
class << self
|
|
8
|
+
# productionでは `rake assets:precompile` が書き出したマニフェストを
|
|
9
|
+
# 読むだけ(メモ化する: プロセスが生きている間、ファイルは変化しない
|
|
10
|
+
# ため)。development/testでは呼び出すたびに現在のシェーダソースから
|
|
11
|
+
# 再構築する。これは config.glslkit.line_directives/minify の
|
|
12
|
+
# dev-vs-production の使い分け方針(SPEC.md §4.1, §4.4)と同じ考え方。
|
|
13
|
+
def manifest
|
|
14
|
+
if ::Rails.env.production?
|
|
15
|
+
@manifest ||= read_precompiled_manifest
|
|
16
|
+
else
|
|
17
|
+
build_and_log_diagnostics
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# developmentでは検証結果をログに出すが、決して例外を投げない
|
|
24
|
+
# (SPEC.md §8.6: 「development では検証するが失敗させない」)。
|
|
25
|
+
# 失敗させるのは `assets:precompile` フック(rails/lib/tasks/glslkit.rake)
|
|
26
|
+
# と `rake glslkit:check` の役目。
|
|
27
|
+
def build_and_log_diagnostics
|
|
28
|
+
builder = ManifestBuilder.new(::Rails.application)
|
|
29
|
+
result = builder.build
|
|
30
|
+
if ::Rails.application.config.glslkit.validate
|
|
31
|
+
builder.diagnostics.each { |diagnostic| ::Rails.logger.warn("[glslkit] #{diagnostic}") }
|
|
32
|
+
end
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def read_precompiled_manifest
|
|
37
|
+
JSON.parse(File.read(manifest_path))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def manifest_path
|
|
41
|
+
config = ::Rails.application.config
|
|
42
|
+
config.assets.output_path.join(config.glslkit.manifest_path)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module Glslkit
|
|
6
|
+
module Rails
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
config.glslkit = ActiveSupport::OrderedOptions.new
|
|
9
|
+
config.glslkit.paths = ["app/shaders"]
|
|
10
|
+
config.glslkit.minify = ::Rails.env.production?
|
|
11
|
+
config.glslkit.line_directives = !::Rails.env.production?
|
|
12
|
+
config.glslkit.manifest_path = "glsl-manifest.json"
|
|
13
|
+
# M11a: SPEC.md §8.6。デフォルトで検証は有効、警告での失敗は無効。
|
|
14
|
+
config.glslkit.validate = true
|
|
15
|
+
config.glslkit.fail_on_warning = false
|
|
16
|
+
config.glslkit.disabled_checks = []
|
|
17
|
+
# M11c: SPEC-livereload.md §3.3。既定はdevelopmentでのみ有効。
|
|
18
|
+
config.glslkit.live_reload = ::Rails.env.development?
|
|
19
|
+
|
|
20
|
+
# Mime::Type.register を使う (Propshaft::Asset#content_type 自身が
|
|
21
|
+
# 実際に使っている経路)。Marcelを直接使うことはしない — MarcelはPropshaft
|
|
22
|
+
# の内部的・推移的な実装詳細に過ぎず、そこに結合すべきではない。
|
|
23
|
+
initializer "glslkit.register_mime_types" do
|
|
24
|
+
::Mime::Type.register "x-shader/x-vertex", :vert unless ::Mime::Type.lookup_by_extension("vert")
|
|
25
|
+
::Mime::Type.register "x-shader/x-fragment", :frag unless ::Mime::Type.lookup_by_extension("frag")
|
|
26
|
+
# register ではなく register_alias を使う: "text/plain" は既に
|
|
27
|
+
# :text として登録済みなので、同じ文字列を :glsl として register で
|
|
28
|
+
# 再登録するとそのlookupエントリを上書きしてしまう。
|
|
29
|
+
::Mime::Type.register_alias "text/plain", :glsl unless ::Mime::Type.lookup_by_extension("glsl")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
initializer "glslkit.configure_propshaft", before: "propshaft.assets_middleware" do |app|
|
|
33
|
+
app.config.glslkit.paths.each do |path|
|
|
34
|
+
app.config.assets.paths << ::Rails.root.join(path).to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
%w[x-shader/x-vertex x-shader/x-fragment text/plain].each do |mime_type|
|
|
38
|
+
app.config.assets.compilers << [mime_type, Glslkit::Rails::Compiler]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
rake_tasks do
|
|
43
|
+
load File.expand_path("../../tasks/glslkit.rake", __dir__)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# M11c: SPEC-livereload.md §3.3。利用者のroutes.rb編集を要求しないため、
|
|
47
|
+
# ミドルウェアとして直接差し込む(`app.routes.append`によるmountは
|
|
48
|
+
# RouteSetのfinalize!タイミングに対してrace conditionがあり、
|
|
49
|
+
# 実機検証で再現性なく404になることが判明したため採用しなかった。
|
|
50
|
+
# 詳細はLiveReload::Mountのコメントを参照)。config.glslkit.live_reload
|
|
51
|
+
# が false(既定でproduction)ならミドルウェア自体を挿入しないため、
|
|
52
|
+
# `/glslkit/*` は通常のルーティングに委ねられ存在しない扱いになる。
|
|
53
|
+
initializer "glslkit.mount_live_reload" do |app|
|
|
54
|
+
next unless app.config.glslkit.live_reload
|
|
55
|
+
|
|
56
|
+
app.middleware.use Glslkit::Rails::LiveReload::Mount, Glslkit::Rails::LiveReload::Engine
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
initializer "glslkit.view_helpers" do
|
|
60
|
+
ActiveSupport.on_load(:action_view) { include Glslkit::Rails::Helper }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
module Rails
|
|
5
|
+
# `config.glslkit.paths` 配下の .vert/.frag ペアを名前ごとに集める。
|
|
6
|
+
# `ManifestBuilder`(静的な precompile)と `LiveReload::ProgramsController`
|
|
7
|
+
# (M11c、ポーリング用のエンドポイント)の両方が全く同じ発見ロジックを
|
|
8
|
+
# 必要とするため、ここに1箇所へ集約する。
|
|
9
|
+
module ShaderPrograms
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def roots(app)
|
|
13
|
+
app.config.glslkit.paths.map { |path| app.root.join(path).to_s }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# {"pbr" => {vertex: "pbr.vert", fragment: "pbr.frag"}, ...} を返す。
|
|
17
|
+
# 同名の相方が無い .vert/.frag はスキップする(共有partialの可能性が
|
|
18
|
+
# あるため。SPEC.md §2.4/§3の「プログラムは両ステージ必須」の決定)。
|
|
19
|
+
def discover(app)
|
|
20
|
+
by_name = Hash.new { |h, k| h[k] = {} }
|
|
21
|
+
|
|
22
|
+
roots(app).each do |root|
|
|
23
|
+
Dir.glob("**/*.{vert,frag}", base: root).each do |relative|
|
|
24
|
+
ext = File.extname(relative)
|
|
25
|
+
stage = (ext == ".vert") ? :vertex : :fragment
|
|
26
|
+
by_name[relative.delete_suffix(ext)][stage] = relative
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
by_name.select { |_, stages| stages.key?(:vertex) && stages.key?(:fragment) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "glslkit"
|
|
4
|
+
require "propshaft"
|
|
5
|
+
require_relative "rails/version"
|
|
6
|
+
require_relative "rails/shader_programs"
|
|
7
|
+
require_relative "rails/compiler"
|
|
8
|
+
require_relative "rails/manifest_builder"
|
|
9
|
+
require_relative "rails/manifest_reader"
|
|
10
|
+
require_relative "rails/helper"
|
|
11
|
+
require_relative "rails/live_reload/known_files"
|
|
12
|
+
require_relative "rails/live_reload/programs_controller"
|
|
13
|
+
require_relative "rails/live_reload/engine"
|
|
14
|
+
require_relative "rails/live_reload/mount"
|
|
15
|
+
require_relative "rails/railtie"
|
|
16
|
+
|
|
17
|
+
module Glslkit
|
|
18
|
+
module Rails
|
|
19
|
+
# 注意: この名前空間の中では、素の定数`Rails`は`Glslkit::Rails`自身に
|
|
20
|
+
# 解決される。トップレベルのRailsフレームワークではない。フレームワーク
|
|
21
|
+
# を指すときは常に `::Rails` と書くこと (例: `::Rails::Engine`)。
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :glslkit do
|
|
4
|
+
desc "Generate the glslkit reflection manifest (config.assets.output_path/config.glslkit.manifest_path)"
|
|
5
|
+
task reflect: :environment do
|
|
6
|
+
app = Rails.application
|
|
7
|
+
builder = Glslkit::Rails::ManifestBuilder.new(app)
|
|
8
|
+
manifest = builder.build
|
|
9
|
+
|
|
10
|
+
# config.glslkit.validateが有効な間は、precompileの入口でビルドを
|
|
11
|
+
# 止める(SPEC.md §8.6)。マニフェストをファイルに書く前に検査すること
|
|
12
|
+
# (壊れたシェーダの情報を含むマニフェストをpublic/に残さないため)。
|
|
13
|
+
if builder.failing?
|
|
14
|
+
message = builder.diagnostics.select(&:error?).join("\n")
|
|
15
|
+
raise Glslkit::ValidationFailed, "glslkit validation failed:\n#{message}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
output_path = app.config.assets.output_path
|
|
19
|
+
FileUtils.mkdir_p(output_path)
|
|
20
|
+
File.write(output_path.join(app.config.glslkit.manifest_path), JSON.generate(manifest))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "Validate all glslkit shaders and exit non-zero on failure (ignores config.glslkit.validate)"
|
|
24
|
+
task check: :environment do
|
|
25
|
+
app = Rails.application
|
|
26
|
+
builder = Glslkit::Rails::ManifestBuilder.new(app)
|
|
27
|
+
builder.build
|
|
28
|
+
|
|
29
|
+
errors = builder.diagnostics.select(&:error?)
|
|
30
|
+
warnings = builder.diagnostics.select(&:warning?)
|
|
31
|
+
|
|
32
|
+
builder.diagnostics.each { |diagnostic| puts diagnostic }
|
|
33
|
+
|
|
34
|
+
# `rake glslkit:check` はconfig.glslkit.validateの設定に関わらず
|
|
35
|
+
# 常に検証結果に基づいて成否を判定する(CI等での明示的な検査用)。
|
|
36
|
+
abort("glslkit: #{errors.size} error(s)") if errors.any?
|
|
37
|
+
abort("glslkit: #{warnings.size} warning(s)") if app.config.glslkit.fail_on_warning && warnings.any?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Propshaft自身のassets:precompile (propshaft/railties/assets.rake) の
|
|
42
|
+
# 後に実行される。そのためRails.application.assets.resolverは、その場での
|
|
43
|
+
# ファイルシステムスキャンではなく、Propshaftが直前に書いた.manifest.json
|
|
44
|
+
# を見て、そこからdigest付きURLを解決する。
|
|
45
|
+
Rake::Task["assets:precompile"].enhance do
|
|
46
|
+
Rake::Task["glslkit:reflect"].invoke
|
|
47
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: glslkit-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.pre
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- kyubey1228
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-08-24 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: glslkit
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.1.0.pre
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 0.2.0
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 0.1.0.pre
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 0.2.0
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: railties
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '7.1'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '7.1'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: actionpack
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '7.1'
|
|
53
|
+
type: :runtime
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '7.1'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: propshaft
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: 1.3.2
|
|
67
|
+
type: :runtime
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 1.3.2
|
|
74
|
+
description: 'Registers .glsl/.vert/.frag assets with Propshaft, resolves #include
|
|
75
|
+
directives at request/precompile time, and publishes a shader reflection manifest
|
|
76
|
+
for consumption by views and JS at runtime.'
|
|
77
|
+
email:
|
|
78
|
+
- kyuuka1228@gmail.com
|
|
79
|
+
executables: []
|
|
80
|
+
extensions: []
|
|
81
|
+
extra_rdoc_files: []
|
|
82
|
+
files:
|
|
83
|
+
- CHANGELOG.md
|
|
84
|
+
- LICENSE.txt
|
|
85
|
+
- README.md
|
|
86
|
+
- config/routes.rb
|
|
87
|
+
- glslkit-rails.gemspec
|
|
88
|
+
- lib/glslkit/rails.rb
|
|
89
|
+
- lib/glslkit/rails/compiler.rb
|
|
90
|
+
- lib/glslkit/rails/helper.rb
|
|
91
|
+
- lib/glslkit/rails/live_reload/engine.rb
|
|
92
|
+
- lib/glslkit/rails/live_reload/known_files.rb
|
|
93
|
+
- lib/glslkit/rails/live_reload/mount.rb
|
|
94
|
+
- lib/glslkit/rails/live_reload/programs_controller.rb
|
|
95
|
+
- lib/glslkit/rails/manifest_builder.rb
|
|
96
|
+
- lib/glslkit/rails/manifest_reader.rb
|
|
97
|
+
- lib/glslkit/rails/railtie.rb
|
|
98
|
+
- lib/glslkit/rails/shader_programs.rb
|
|
99
|
+
- lib/glslkit/rails/version.rb
|
|
100
|
+
- lib/tasks/glslkit.rake
|
|
101
|
+
homepage: https://github.com/kyubey1228/glslkit
|
|
102
|
+
licenses:
|
|
103
|
+
- MIT
|
|
104
|
+
metadata:
|
|
105
|
+
homepage_uri: https://github.com/kyubey1228/glslkit
|
|
106
|
+
source_code_uri: https://github.com/kyubey1228/glslkit/tree/main/rails
|
|
107
|
+
changelog_uri: https://github.com/kyubey1228/glslkit/blob/main/rails/CHANGELOG.md
|
|
108
|
+
rdoc_options: []
|
|
109
|
+
require_paths:
|
|
110
|
+
- lib
|
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '3.1'
|
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
requirements: []
|
|
122
|
+
rubygems_version: 3.6.2
|
|
123
|
+
specification_version: 4
|
|
124
|
+
summary: Rails (Propshaft) integration for glslkit
|
|
125
|
+
test_files: []
|