Skip to content

Instantly share code, notes, and snippets.

@terasakisatoshi
Created August 25, 2025 05:22
Show Gist options
  • Save terasakisatoshi/275c26f2b628c6e9f62db204b8883db6 to your computer and use it in GitHub Desktop.
Save terasakisatoshi/275c26f2b628c6e9f62db204b8883db6 to your computer and use it in GitHub Desktop.
WebGL Editor
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGL Shader Editor</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Consolas', 'Monaco', monospace;
background: #2d2c2e;
color: #fcfcfa;
height: 100vh;
overflow: hidden;
}
.container {
display: flex;
height: 100vh;
}
.editor-panel {
flex: 1;
display: flex;
flex-direction: column;
border-right: 2px solid #19181a;
}
.preview-panel {
flex: 1;
display: flex;
flex-direction: column;
background: #221f22;
}
.tabs {
display: flex;
background: #2d2c2e;
border-bottom: 1px solid #19181a;
}
.tab {
padding: 12px 24px;
cursor: pointer;
background: transparent;
color: #939293;
border: none;
border-bottom: 2px solid transparent;
transition: all 0.3s;
font-size: 13px;
}
.tab:hover {
color: #fcfcfa;
}
.tab.active {
color: #ffd866;
background: #19181a;
border-bottom-color: #ffd866;
}
.editor-container {
flex: 1;
position: relative;
overflow: hidden;
}
.editor {
width: 100%;
height: 100%;
padding: 15px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
line-height: 1.5;
background: #19181a;
color: #fcfcfa;
border: none;
outline: none;
resize: none;
tab-size: 2;
}
#canvas {
flex: 1;
background: #000;
image-rendering: crisp-edges;
}
.controls {
padding: 15px;
background: #2d2c2e;
border-top: 1px solid #19181a;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
button {
padding: 8px 16px;
background: #ff6188;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
button:hover {
background: #ff4570;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(255, 97, 136, 0.3);
}
button:active {
transform: translateY(0);
}
.error-console {
position: fixed;
bottom: 0;
left: 0;
right: 0;
max-height: 150px;
background: #19181a;
border-top: 2px solid #ff6188;
padding: 15px;
color: #ff6188;
font-family: monospace;
font-size: 13px;
overflow-y: auto;
transform: translateY(100%);
transition: transform 0.3s;
z-index: 1000;
white-space: pre-wrap;
}
.error-console.show {
transform: translateY(0);
}
.status {
flex: 1;
text-align: right;
color: #939293;
font-size: 12px;
}
.status.success {
color: #a9dc76;
}
.presets {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.preset-btn {
padding: 6px 12px;
font-size: 12px;
background: #403e41;
}
.preset-btn:hover {
background: #4d4b4e;
}
.time-info {
color: #ffd866;
font-size: 12px;
margin-left: 10px;
}
/* スクロールバーのスタイリング */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: #19181a;
}
::-webkit-scrollbar-thumb {
background: #403e41;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #4d4b4e;
}
</style>
</head>
<body>
<div class="container">
<div class="editor-panel">
<div class="tabs">
<button class="tab active" data-shader="vertex">Vertex Shader</button>
<button class="tab" data-shader="fragment">Fragment Shader</button>
</div>
<div class="editor-container">
<textarea id="vertexShader" class="editor" spellcheck="false">attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}</textarea>
<textarea id="fragmentShader" class="editor" style="display: none;" spellcheck="false">precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
varying vec2 v_texCoord;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution;
// グラデーション + 時間アニメーション
vec3 color = vec3(0.0);
color.r = sin(st.x * 3.14159 + u_time) * 0.5 + 0.5;
color.g = sin(st.y * 3.14159 + u_time * 1.3) * 0.5 + 0.5;
color.b = sin((st.x + st.y) * 3.14159 + u_time * 0.7) * 0.5 + 0.5;
// マウス位置の影響
vec2 mouse = u_mouse / u_resolution;
float dist = length(st - mouse);
color *= 1.0 - dist * 0.5;
gl_FragColor = vec4(color, 1.0);
}</textarea>
</div>
</div>
<div class="preview-panel">
<canvas id="canvas"></canvas>
<div class="controls">
<button id="compileBtn">コンパイル & 実行</button>
<div class="presets">
<button class="preset-btn" data-preset="reactionDiffusion">反応拡散</button>
<button class="preset-btn" data-preset="wave">Wave</button>
<button class="preset-btn" data-preset="plasma">Plasma</button>
<button class="preset-btn" data-preset="raymarching">Raymarching</button>
<button class="preset-btn" data-preset="mandelbrot">Mandelbrot</button>
</div>
<span class="time-info">Time: <span id="timeDisplay">0.00</span></span>
<div class="status" id="status">Ready</div>
</div>
</div>
</div>
<div class="error-console" id="errorConsole"></div>
<script>
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
const vertexShaderEditor = document.getElementById('vertexShader');
const fragmentShaderEditor = document.getElementById('fragmentShader');
const errorConsole = document.getElementById('errorConsole');
const status = document.getElementById('status');
const timeDisplay = document.getElementById('timeDisplay');
let program = null;
let startTime = Date.now();
let mouseX = 0, mouseY = 0;
let animationId = null;
// プリセットシェーダー
const presets = {
reactionDiffusion: {
fragment: `precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
varying vec2 v_texCoord;
// 擬似乱数生成
float random(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}
// スムーズノイズ
float noise(vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
// フラクタルブラウン運動
float fbm(vec2 st) {
float value = 0.0;
float amplitude = 0.5;
for(int i = 0; i < 5; i++) {
value += amplitude * noise(st);
st *= 2.0;
amplitude *= 0.5;
}
return value;
}
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
vec2 mouse = u_mouse / u_resolution;
// 座標を中心基準に
vec2 pos = st - 0.5;
// 時間でアニメーション
float t = u_time * 0.5;
// 歪みの計算
float n1 = fbm(st * 3.0 + vec2(t * 0.1, t * 0.15));
float n2 = fbm(st * 5.0 - vec2(t * 0.2, -t * 0.1));
// 反応拡散風のパターン生成
vec2 distorted = st + vec2(n1, n2) * 0.1;
// 複数のパターンレイヤー
float pattern1 = sin(distorted.x * 30.0 + t) * cos(distorted.y * 30.0 - t * 0.7);
float pattern2 = fbm(distorted * 10.0 + t * 0.2);
float pattern3 = noise(distorted * 50.0 - t * 0.5);
// スポット生成
float spots = 0.0;
for(float i = 0.0; i < 5.0; i++) {
vec2 spotPos = vec2(
0.5 + sin(t * 0.3 + i * 1.5) * 0.3,
0.5 + cos(t * 0.4 + i * 2.1) * 0.3
);
float dist = length(st - spotPos);
spots += 1.0 / (1.0 + dist * 20.0);
}
// マウス影響
float mouseDist = length(st - mouse);
float mouseEffect = exp(-mouseDist * 5.0);
// パターンの合成
float finalPattern = pattern1 * 0.3 + pattern2 * 0.5 + pattern3 * 0.2;
finalPattern = sin(finalPattern * 10.0 + spots * 2.0 + mouseEffect * 3.0);
finalPattern = smoothstep(-0.2, 0.2, finalPattern);
// 有機的な色付け
vec3 color1 = vec3(0.1, 0.2, 0.3); // 深い青
vec3 color2 = vec3(0.9, 0.7, 0.3); // 暖かい黄色
vec3 color3 = vec3(0.3, 0.8, 0.6); // ティール
// グラデーションマッピング
vec3 color = mix(color1, color2, finalPattern);
color = mix(color, color3, spots * 0.3);
// 波紋エフェクト
float ripple = sin(length(pos) * 20.0 - t * 3.0) * 0.5 + 0.5;
color += ripple * 0.1 * (1.0 - smoothstep(0.2, 0.5, length(pos)));
// コントラスト調整
color = pow(color, vec3(0.8));
gl_FragColor = vec4(color, 1.0);
}`
},
wave: {
fragment: `precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution;
// 波のパラメータ
float frequency = 10.0;
float amplitude = 0.1;
float speed = 2.0;
float wave = sin(st.x * frequency + u_time * speed) * amplitude;
st.y += wave;
// カラーグラデーション
vec3 color = vec3(0.0);
color.r = st.x;
color.g = st.y;
color.b = abs(sin(u_time));
gl_FragColor = vec4(color, 1.0);
}`
},
plasma: {
fragment: `precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution;
float v = 0.0;
vec2 c = st * 10.0 - vec2(5.0);
// プラズマエフェクト
v += sin((c.x + u_time));
v += sin((c.y + u_time) / 2.0);
v += sin((c.x + c.y + u_time) / 2.0);
c += vec2(sin(u_time / 3.0), cos(u_time / 2.0)) * 2.0;
v += sin(sqrt(c.x * c.x + c.y * c.y + 1.0) + u_time);
v = v / 2.0;
// カラーマッピング
vec3 color = vec3(1.0, sin(v * 3.14159), cos(v * 3.14159));
gl_FragColor = vec4(color * 0.5 + 0.5, 1.0);
}`
},
raymarching: {
fragment: `precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
varying vec2 v_texCoord;
// 球の距離関数
float sdSphere(vec3 p, float r) {
return length(p) - r;
}
// シーンの距離関数
float map(vec3 p) {
float d = sdSphere(p, 1.0);
vec3 p2 = p;
p2.x += sin(u_time) * 0.5;
float d2 = sdSphere(p2 - vec3(2.0, 0.0, 0.0), 0.5);
return min(d, d2);
}
// 法線の計算
vec3 calcNormal(vec3 p) {
const float eps = 0.001;
return normalize(vec3(
map(p + vec3(eps, 0.0, 0.0)) - map(p - vec3(eps, 0.0, 0.0)),
map(p + vec3(0.0, eps, 0.0)) - map(p - vec3(0.0, eps, 0.0)),
map(p + vec3(0.0, 0.0, eps)) - map(p - vec3(0.0, 0.0, eps))
));
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
vec2 mouse = (u_mouse - u_resolution * 0.5) / u_resolution.y;
// カメラ設定
vec3 ro = vec3(mouse * 3.0, 3.0);
vec3 rd = normalize(vec3(uv, -1.0));
// レイマーチング
float t = 0.0;
for(int i = 0; i < 64; i++) {
vec3 p = ro + rd * t;
float d = map(p);
if(d < 0.001) break;
t += d;
if(t > 20.0) break;
}
// シェーディング
vec3 color = vec3(0.0);
if(t < 20.0) {
vec3 p = ro + rd * t;
vec3 n = calcNormal(p);
vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
float diff = max(dot(n, lightDir), 0.0);
color = vec3(diff);
color *= vec3(0.5, 0.7, 1.0);
}
gl_FragColor = vec4(color, 1.0);
}`
},
mandelbrot: {
fragment: `precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
varying vec2 v_texCoord;
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 mouse = u_mouse / u_resolution;
// ズームとパン
float zoom = exp(-mouse.y * 3.0) * 2.0;
vec2 c = (uv - 0.5) * zoom + vec2(-0.5 + mouse.x * 0.5, 0.0);
// マンデルブロ集合の計算
vec2 z = vec2(0.0);
int iter = 0;
const int maxIter = 100;
for(int i = 0; i < maxIter; i++) {
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;
if(length(z) > 2.0) break;
iter++;
}
// カラーマッピング
vec3 color = vec3(0.0);
if(iter < maxIter) {
float f = float(iter) / float(maxIter);
color = hsv2rgb(vec3(f * 0.6 + u_time * 0.1, 0.8, 1.0));
}
gl_FragColor = vec4(color, 1.0);
}`
}
};
// タブ切り替え
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', (e) => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
e.target.classList.add('active');
const shaderType = e.target.dataset.shader;
if (shaderType === 'vertex') {
vertexShaderEditor.style.display = 'block';
fragmentShaderEditor.style.display = 'none';
} else {
vertexShaderEditor.style.display = 'none';
fragmentShaderEditor.style.display = 'block';
}
});
});
// プリセットボタン
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const preset = presets[e.target.dataset.preset];
if (preset) {
if (preset.vertex) {
vertexShaderEditor.value = preset.vertex;
}
if (preset.fragment) {
fragmentShaderEditor.value = preset.fragment;
// Fragment Shaderタブに切り替え
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelector('[data-shader="fragment"]').classList.add('active');
vertexShaderEditor.style.display = 'none';
fragmentShaderEditor.style.display = 'block';
}
compile();
}
});
});
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const error = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error(error);
}
return shader;
}
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const error = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new Error(error);
}
return program;
}
function compile() {
try {
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderEditor.value);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderEditor.value);
if (program) {
gl.deleteProgram(program);
}
program = createProgram(gl, vertexShader, fragmentShader);
errorConsole.classList.remove('show');
status.textContent = 'コンパイル成功!';
status.classList.add('success');
startTime = Date.now();
if (animationId) {
cancelAnimationFrame(animationId);
}
render();
} catch (error) {
errorConsole.textContent = error.message;
errorConsole.classList.add('show');
status.textContent = 'エラー';
status.classList.remove('success');
}
}
function setupGeometry() {
const positions = new Float32Array([
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]);
const texCoords = new Float32Array([
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
]);
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);
const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
if (texCoordLocation >= 0) {
gl.enableVertexAttribArray(texCoordLocation);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
}
}
function render() {
if (!program) return;
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
setupGeometry();
const timeLocation = gl.getUniformLocation(program, 'u_time');
const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
const mouseLocation = gl.getUniformLocation(program, 'u_mouse');
const currentTime = (Date.now() - startTime) / 1000;
timeDisplay.textContent = currentTime.toFixed(2);
if (timeLocation) {
gl.uniform1f(timeLocation, currentTime);
}
if (resolutionLocation) {
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
}
if (mouseLocation) {
gl.uniform2f(mouseLocation, mouseX, canvas.height - mouseY);
}
gl.drawArrays(gl.TRIANGLES, 0, 6);
animationId = requestAnimationFrame(render);
}
// マウス追跡
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
});
// コンパイルボタン
document.getElementById('compileBtn').addEventListener('click', compile);
// Ctrl+Enterでコンパイル
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'Enter') {
compile();
}
});
// 初期コンパイル
compile();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment