シェーダー表示
code:module.js
import loadScript from '/api/code/src-wintyo/library/loadScript.js';
// デフォルトフラグメントシェーダー
const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(0, 0, 0, 1.0);
}
`;
// デフォルトバーテックスシェーダー
const VERTEX_SHADER = `
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
export default class SharderViewer {
constructor() {
// 描画中か
this.isRendering = false;
// キャプチャ中か
this.isCapturing = false;
// シェーダーに渡す変数
this.uniforms = {
u_mouse: { value: { x: 0.0, y: 0.0 } },
u_resolution: { value: { x: window.innerWidth, y: window.innerHeight } },
u_time: { value: 0.0 },
};
}
/** 初期化 */
async setup(elCanvas) {
// ライブラリの読み込み
await loadScript('three-js-script', src);
const src2 = '/api/code/src-wintyo/CCapture/script.js';
await loadScript('ccapture-script', src2);
this.capturer = new CCapture({
name: 'shader',
format: 'webm',
// MIME typeがtext/plainで読み込み実行ができないため動かせない
// format: 'gif',
// workersPath: '/api/code/src-wintyo/CCapture/',
// originが違うのでそもそも無理
});
this.elCanvas = elCanvas;
// シーンを作成
this.scene = new THREE.Scene();
// カメラを作成
this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1);
this.camera.position.z = 1;
// レンダラーを作成
this.renderer = new THREE.WebGLRenderer({
canvas: elCanvas,
antialias: true,
preserveDrawingBuffer: true,
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
// 平面を作成
const geometry = new THREE.PlaneGeometry(2, 2);
// マテリアルを作成
const material = new THREE.ShaderMaterial();
// メッシュを作成
this.mesh = new THREE.Mesh(geometry, material);
// 平面をシーンに追加
this.scene.add(this.mesh);
// 時間を計測する
this.clock = new THREE.Clock();
}
/** 描画 */
render() {
console.log('render');
this.uniforms.u_time.value = this.clock.getElapsedTime();
this.renderer.render(this.scene, this.camera);
if (this.isCapturing) {
this.capturer.capture(this.elCanvas);
}
if (this.isRendering) {
requestAnimationFrame(this.render.bind(this));
}
}
/** 描画の開始 */
startRendering() {
if (this.isRendering) {
return;
}
// マウス座標の保存
document.addEventListener('mousemove', this._mousemoveHandler = (event) => {
this.uniforms.u_mouse.value.x = event.clientX;
this.uniforms.u_mouse.value.y = event.clientY;
});
// リサイズ処理
window.addEventListener('resize', this._resizeHandler = () => {
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.uniforms.u_resolution.value.x = window.innerWidth;
this.uniforms.u_resolution.value.y = window.innerHeight;
});
this._resizeHandler();
this.isRendering = true;
this.render();
}
/** 描画のストップ */
stopRendering() {
if (!this.isRendering) {
return;
}
document.removeEventListener('mousemove', this._mousemoveHandler);
window.removeEventListener('resize', this._resizeHandler);
this.isRendering = false;
}
/** キャプチャの開始 */
startCapture() {
if (!this.isRendering) {
alert('描画中では無いのでキャプチャできません。');
return;
}
this.isCapturing = true;
this.capturer.start();
}
/** キャプチャの終了 */
stopCapture() {
this.isCapturing = false;
this.capturer.stop();
this.capturer.save();
}
/**
* シェーダーの更新
* @param fragmentShader - フラグメントシェーダー
* @param vertexShader - バーテックスシェーダー
*/
updateShader(fragmentShader, vertexShader) {
this.mesh.material = new THREE.ShaderMaterial({
uniforms: this.uniforms,
fragmentShader: fragmentShader || FRAGMENT_SHADER,
vertexShader: vertexShader || VERTEX_SHADER,
});
}
}
code:script.js
import ShaderViewer from '/api/code/src-wintyo/シェーダー表示/module.js';
// DOMに登録
const $canvas = $('<canvas>');
$canvas.css({
display: 'none',
position: 'fixed',
zIndex: '-1',
top: 0,
left: 0,
});
$('#app-container').append($canvas);
(async () => {
const MENU_TITLE = 'Shader';
const viewer = new ShaderViewer();
await viewer.setup($canvas.get(0));
const loadShader = async () => {
const pagePath = (() => {
const match = location.href.match(/https:\/\/scrapbox.io\/(a-zA-Z0-9()%/-+)$/); if (!match) {
throw new Error('cannot get scrapbox path');
}
})();
const fragmentShader = await fetch(/api/code/${pagePath}/fragment-shader.glsl)
.then((req) => req.text())
.catch(() => undefined);
const vertexShader = await fetch(/api/code/${pagePath}/vertex-shader.glsl)
.then((req) => req.text())
.catch(() => undefined);
viewer.updateShader(fragmentShader, vertexShader);
};
scrapbox.PageMenu.addMenu({
title: MENU_TITLE,
});
scrapbox.PageMenu(MENU_TITLE).addItem({
title: () => viewer.isRendering ? '描画停止' : '描画開始',
onClick: async () => {
if (viewer.isRendering) {
viewer.stopRendering();
return;
}
await loadShader();
viewer.startRendering();
$canvas.show();
},
});
scrapbox.PageMenu(MENU_TITLE).addItem({
title: () => 'スクリーンショット',
onClick: () => {
const elLink = document.createElement('a');
elLink.href = $canvas.get(0).toDataURL('image/png');
elLink.download = 'shader.png';
elLink.click();
},
});
scrapbox.PageMenu(MENU_TITLE).addItem({
title: () => viewer.isCapturing ? 'キャプチャ終了' : 'キャプチャ開始',
onClick: () => {
if (viewer.isCapturing) {
viewer.stopCapture();
return;
}
viewer.startCapture();
},
});
scrapbox.PageMenu(MENU_TITLE).addItem({
title: () => $('.page').hasClass('shader-opacity') ? 'ページ非透過' : 'ページ透過',
onClick: () => {
$('.page').toggleClass('shader-opacity');
},
});
})();
code:style.css
.shader-opacity {
opacity: 0.5;
}
https://gyazo.com/bbadba8fa6f8f2564a12844a223074dc
code:fragment-shader.glsl
uniform vec2 u_mouse;
uniform vec2 u_resolution;
void main() {
vec3 color = vec3(u_mouse.x / u_resolution.x, u_mouse.y / u_resolution.y, 0.0);
gl_FragColor = vec4(color, 1.0);
}
code:vertex-shader.glsl
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
これが使えれば良かったがセキュリティエラーが出る
参考
最終的にはThree.jsで力技でシェーダーが使える環境を用意した
アニメーションキャプチャはwebmで行われるが、gifでキャプチャしたい場合は以下のリポジトリを参考にする。