Created
August 25, 2025 05:22
-
-
Save terasakisatoshi/275c26f2b628c6e9f62db204b8883db6 to your computer and use it in GitHub Desktop.
WebGL Editor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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