glslkit-rails
glslkit を Rails (Propshaft) のアセットパイプラインに統合する gem。
.glsl / .vert / .frag をアセットとして登録し、#include を解決した内容を
配信し、assets:precompile 時にリフレクションマニフェストを書き出す。
インストール
gem "glslkit-rails"
glslkit・railties >= 7.1・propshaft >= 1.3.2 に依存する。
設定
# config/application.rb
config.glslkit.paths = ["app/shaders"] # デフォルト: app/shaders
config.glslkit.minify = Rails.env.production?
config.glslkit.line_directives = !Rails.env.production?
config.glslkit.manifest_path = "glsl-manifest.json"
アセットとしての配信
config.glslkit.paths 配下のファイルはPropshaftのload_pathに追加され、
#include を解決した内容がcontent_type x-shader/x-vertex /
x-shader/x-fragment / text/plain で配信される。developmentでは
毎リクエスト再処理し、productionでは assets:precompile 時に1回だけ処理する。
マニフェスト
rake assets:precompile を実行すると、自動的に
public/assets/<config.glslkit.manifest_path> (デフォルト glsl-manifest.json)
にリフレクションマニフェストを書き出す。単体で実行する場合:
rake glslkit:reflect
Ruby側から読む場合:
Glslkit::Rails.manifest # => Hash (productionでは読み込み結果をメモ化する)
ビューヘルパー
<%= glsl_script_tag "pbr.vert" %>
<%# => <script type="x-shader/x-vertex" id="pbr-vert">...</script> (インライン埋め込み) %>
<%= glsl_manifest_tag %>
<%# => <script type="application/json" id="glsl-manifest">...</script> %>
- インライン埋め込みの内容は
</scriptをエスケープしている - そのリクエストでCSPが実際に有効な場合(
content_security_policy?)、 自動的にnonce属性を付与する。nonce: true/falseで強制・抑止も できる
実践的な例
app/shaders/ にPBR風のマテリアルシェーダーを置く。共有partialの
uniform block、複数テクスチャ、uniform配列を含む、より現実的な構成:
# app/shaders/common/camera.glsl
layout(std140, binding = 0) uniform Camera {
mat4 view;
mat4 projection;
vec3 eye_position;
};
# app/shaders/material.vert
#version 300 es
#include "common/camera.glsl"
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec3 a_normal;
layout(location = 2) in vec2 a_uv;
uniform mat4 u_model;
out vec3 v_world_position;
out vec3 v_normal;
out vec2 v_uv;
void main() {
vec4 world_position = u_model * vec4(a_position, 1.0);
v_world_position = world_position.xyz;
v_normal = mat3(u_model) * a_normal;
v_uv = a_uv;
gl_Position = projection * view * world_position;
}
# app/shaders/material.frag
#version 300 es
precision highp float;
#include "common/camera.glsl"
in vec3 v_world_position;
in vec3 v_normal;
in vec2 v_uv;
uniform sampler2D u_albedo_map;
uniform vec4 u_base_color;
uniform vec3 u_light_positions[4];
uniform vec3 u_light_colors[4];
uniform int u_light_count;
out vec4 fragColor;
void main() {
vec3 albedo = texture(u_albedo_map, v_uv).rgb * u_base_color.rgb;
vec3 n = normalize(v_normal);
vec3 total = vec3(0.0);
for (int i = 0; i < u_light_count; i++) {
vec3 to_light = normalize(u_light_positions[i] - v_world_position);
total += albedo * u_light_colors[i] * max(dot(n, to_light), 0.0);
}
fragColor = vec4(total, u_base_color.a);
}
ビューでは平坦化済みシェーダーとマニフェストをまとめて埋め込む:
<%# app/views/scenes/show.html.erb %>
<%= glsl_script_tag "material.vert" %>
<%= glsl_script_tag "material.frag" %>
<%= glsl_manifest_tag %>
glsl_script_tag "material.vert" は #include "common/camera.glsl" を
解決した平坦化済みGLSLを id="material-vert" として埋め込み、
glsl_manifest_tag は attribute の location・uniform の型/setter/配列サイズ・
uniform blockのlayout/bindingを持つJSONを id="glsl-manifest" として埋め込む。
JS側は、このマニフェストを消費するだけでWebGLプログラムをセットアップできる。
gl.getActiveUniform/gl.getActiveAttrib でイントロスペクションするループが
不要になるのがglslkitの狙い:
function compileProgram(gl, manifestProgram, shaderElementIds) {
const vertexShader = compileShader(gl, gl.VERTEX_SHADER,
document.getElementById(shaderElementIds.vertex).textContent);
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER,
document.getElementById(shaderElementIds.fragment).textContent);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program));
}
const attributes = {};
for (const attr of manifestProgram.attributes) {
// location: null の場合だけ getAttribLocation を引けばよい
attributes[attr.name] = attr.location ?? gl.getAttribLocation(program, attr.name);
}
const uniforms = {};
let textureUnit = 0;
for (const uniform of manifestProgram.uniforms) {
const location = gl.getUniformLocation(program, uniform.name);
if (!location) continue; // 未使用uniformはコンパイラに削除されうる
if (uniform.sampler) {
const unit = textureUnit++;
uniforms[uniform.name] = (texture) => {
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(location, unit);
};
} else if (uniform.matrix) {
uniforms[uniform.name] = (value) => gl[uniform.setter](location, false, value);
} else {
uniforms[uniform.name] = (value) => gl[uniform.setter](location, value);
}
}
return {program, attributes, uniforms};
}
function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(shader));
}
return shader;
}
const manifest = JSON.parse(document.getElementById("glsl-manifest").textContent);
const material = compileProgram(gl, manifest.programs.material, {
vertex: "material-vert",
fragment: "material-frag",
});
material.uniforms.u_model(modelMatrix);
material.uniforms.u_base_color(new Float32Array([1, 1, 1, 1]));
material.uniforms.u_albedo_map(albedoTexture);
uniform.setter/uniform.matrix/uniform.samplerはすべて
Glslkit::Types由来で、spec/schema/reflection-v1.json
に準拠する。