Files
shaders/build_plasma.py
T
emil28092005 27d238cbe6 Ice slab with layered plasma, two-pass refraction
Pass 1 renders eight plasma sheets into an atlas with linear depth in alpha;
pass 2 marches the refracted ray to each sheet and absorbs it by real path
length, so near and far layers separate under pointer parallax. Plasma cycle
repeats exactly every 30 s; - / = change speed.
2026-10-30 19:56:16 +03:00

124 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Embed the two GLSL passes into the standalone ice_plasma.html page."""
import json
from pathlib import Path
here = Path(__file__).resolve().parent
scene = (here / '6_ice_plasma.frag').read_text()
ice = (here / '7_ice_over.frag').read_text()
html = r'''<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Сквозь лёд</title>
<style>
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#03111d}
canvas{display:block;width:100%;height:100%;touch-action:none}
#info{position:fixed;bottom:20px;left:24px;color:#d8f4ff;font:13px/1.6 system-ui,sans-serif;text-shadow:0 1px 5px #001;pointer-events:none}
#info strong{font-weight:500} #info span{opacity:.75}
</style>
</head>
<body>
<canvas id="view" aria-label="Плазма под прозрачным льдом. Двигайте указатель для параллакса."></canvas>
<div id="info" role="status"><strong>Сквозь лёд</strong><br><span id="help">Двигайте мышь · скорость − / =</span></div>
<script>
'use strict';
const sceneSource = __SCENE__;
const iceSource = __ICE__;
const vertexSource = 'attribute vec2 p; void main(){gl_Position=vec4(p,0.0,1.0);}';
const canvas = document.getElementById('view'), help = document.getElementById('help');
const gl = canvas.getContext('webgl', {antialias:false, alpha:false, preserveDrawingBuffer:true});
function fail(message) { help.textContent = message; throw new Error(message); }
if (!gl) fail('WebGL недоступен в этом браузере.');
function program(source) {
function shader(type, code) {
const s = gl.createShader(type);
gl.shaderSource(s, code); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) fail('Ошибка шейдера: ' + gl.getShaderInfoLog(s));
return s;
}
const p = gl.createProgram(), vs = shader(gl.VERTEX_SHADER, vertexSource), fs = shader(gl.FRAGMENT_SHADER, source);
gl.attachShader(p, vs); gl.attachShader(p, fs); gl.linkProgram(p);
gl.deleteShader(vs); gl.deleteShader(fs);
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) fail('Ошибка программы: ' + gl.getProgramInfoLog(p));
return p;
}
const scene = program(sceneSource), ice = program(iceSource);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW);
let texture, framebuffer, tileWidth, tileHeight;
function resize() {
const max = gl.getParameter(gl.MAX_TEXTURE_SIZE);
const dpr = Math.min(devicePixelRatio || 1, 1.25, max / (innerWidth * 3), max / (innerHeight * 1.5));
canvas.width = Math.max(4, Math.floor(innerWidth * dpr / 4) * 4);
canvas.height = Math.max(4, Math.floor(innerHeight * dpr / 4) * 4);
tileWidth = canvas.width * 3 / 4; tileHeight = canvas.height * 3 / 4;
if (texture) gl.deleteTexture(texture);
if (framebuffer) gl.deleteFramebuffer(framebuffer);
texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, tileWidth * 4, tileHeight * 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
framebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) fail('Не удалось создать текстуру сцены.');
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
addEventListener('resize', resize); resize();
const shot = location.hash.startsWith('#shot');
const shotParams = new URLSearchParams(location.hash.slice(1));
let mouseX = Number(shotParams.get('mx') ?? 0.5), mouseY = Number(shotParams.get('my') ?? 0.5);
let speed = 1, time = Number(shotParams.get('t') ?? 0), previous;
addEventListener('pointermove', e => { mouseX = e.clientX / innerWidth; mouseY = 1 - e.clientY / innerHeight; });
addEventListener('keydown', e => {
if (e.key === '-' || e.key === '_') speed = Math.max(0.125, speed / 2);
else if (e.key === '=' || e.key === '+') speed = Math.min(4, speed * 2);
else return;
help.textContent = 'Двигайте мышь · скорость ' + speed + '× (− / =)';
});
function draw(p, width, height) {
gl.useProgram(p);
const position = gl.getAttribLocation(p, 'p');
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.enableVertexAttribArray(position);
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
gl.uniform2f(gl.getUniformLocation(p, 'u_res'), width, height);
gl.uniform1f(gl.getUniformLocation(p, 'u_time'), time);
gl.uniform2f(gl.getUniformLocation(p, 'u_mouse'), mouseX, mouseY);
gl.uniform2f(gl.getUniformLocation(p, 'u_tile_res'), tileWidth, tileHeight);
gl.uniform1i(gl.getUniformLocation(p, 'u_scene'), 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
function frame(now) {
if (previous !== undefined) time += (now - previous) * 0.001 * speed;
previous = now;
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.viewport(0, 0, tileWidth * 4, tileHeight * 2);
draw(scene, tileWidth, tileHeight);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture);
draw(ice, canvas.width, canvas.height);
if (shot) {
// Headless Firefox does not composite WebGL; expose the actual rendered pixels.
const pixels = new Uint8Array(canvas.width * canvas.height * 4);
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
const output = document.createElement('canvas');
output.width = canvas.width; output.height = canvas.height;
const ctx = output.getContext('2d'), image = ctx.createImageData(output.width, output.height);
for (let y = 0; y < output.height; y++)
image.data.set(pixels.subarray((output.height - y - 1) * output.width * 4, (output.height - y) * output.width * 4), y * output.width * 4);
ctx.putImageData(image, 0, 0); canvas.replaceWith(output);
} else requestAnimationFrame(frame);
}
if (shot) frame(0); else requestAnimationFrame(frame);
</script>
</body>
</html>
'''
(here / 'ice_plasma.html').write_text(html.replace('__SCENE__', json.dumps(scene)).replace('__ICE__', json.dumps(ice)))
print('ice_plasma.html written')