ShaderToy中如何每帧调用迭代函数?Verlet积分两体模拟遇阻
ShaderToy中Verlet积分两体模拟的帧更新方案
核心问题:ShaderToy的无状态特性
ShaderToy的片段着色器每帧都会重新运行,没有内置的持久化状态。直接在主函数里循环调用更新函数,只会在首帧重复执行,后续帧无法继承之前的状态——这就是你遇到“要么首帧多次跑,要么只跑一次”的原因。
解决方法:用反馈纹理(iChannel10)存储状态
iChannel10是ShaderToy专门用于帧间反馈的通道:把上一帧渲染的结果(包含物理状态数据)作为这一帧的输入,以此实现状态的持续更新。具体步骤如下:
1. 定义状态数据格式
将两体的位置、历史位置(Verlet积分必需)打包成纹理像素数据,比如:
- 第一个物体:R=当前x坐标,G=当前y坐标,B=历史x坐标,A=历史y坐标
- 第二个物体:用另一个像素存储(可将纹理设为2x1大小,每个像素对应一个物体)
2. 初始化首帧状态
当iFrame == 0时,为两个物体设置初始位置和历史位置(比如给初始位置加微小偏移模拟初始速度)。
3. 每帧执行Verlet更新
从iChannel10读取上一帧的状态,对每个物体执行Verlet积分计算:
vec2 verletUpdate(vec2 currPos, vec2 prevPos, vec2 acceleration, float dt) { // Verlet核心公式:新位置 = 2*当前位置 - 历史位置 + 加速度*dt² vec2 newPos = 2.0 * currPos - prevPos + acceleration * dt * dt; return newPos; }
这里的dt建议用固定值(比如1.0/60.0,对应60帧每秒),避免用iTime这类连续值导致离散步骤不稳定。
4. 输出新状态到反馈纹理
把计算后的新位置、原当前位置(作为下一帧的历史位置)重新打包成像素数据,输出为片段颜色——iChannel10会自动将当前帧的输出作为下一帧的输入。
关键代码示例
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { // 用2x1纹理存储两个物体的状态 vec2 uv = fragCoord.xy / iResolution.xy; // 读取上一帧的状态数据 vec4 prevState1 = texture(iChannel10, vec2(0.25, 0.5)); vec4 prevState2 = texture(iChannel10, vec2(0.75, 0.5)); vec2 currPos1, prevPos1, currPos2, prevPos2; float dt = 1.0 / 60.0; // 固定步长 // 首帧初始化状态 if(iFrame == 0) { currPos1 = vec2(0.3, 0.5); prevPos1 = currPos1 + vec2(0.001, 0.0); // 给第一个物体初始向右的速度 currPos2 = vec2(0.7, 0.5); prevPos2 = currPos2 - vec2(0.001, 0.0); // 给第二个物体初始向左的速度 } else { currPos1 = prevState1.rg; prevPos1 = prevState1.ba; currPos2 = prevState2.rg; prevPos2 = prevState2.ba; } // 计算两体间的引力加速度 vec2 delta = currPos2 - currPos1; float dist = length(delta); float force = 0.001 / (dist * dist); // 简化的引力公式 vec2 accel1 = normalize(delta) * force; vec2 accel2 = -accel1; // 作用力与反作用力 // 执行Verlet更新 vec2 newPos1 = verletUpdate(currPos1, prevPos1, accel1, dt); vec2 newPos2 = verletUpdate(currPos2, prevPos2, accel2, dt); // 输出新状态到反馈纹理 if(uv.x < 0.5) { fragColor = vec4(newPos1, currPos1); // RG存新位置,BA存旧位置(下一帧的历史位置) } else { fragColor = vec4(newPos2, currPos2); } // 可选:在屏幕上绘制两个物体 vec2 screenPos = fragCoord.xy / iResolution.xy; float dotSize = 0.02; if(length(screenPos - newPos1) < dotSize || length(screenPos - newPos2) < dotSize) { fragColor = vec4(1.0, 0.0, 0.0, 1.0); } }
iChannel10的设置方法
在ShaderToy右侧的Channels面板中,把Channel 10的Input设置为Buffer,然后选择当前这个Shader本身——这样就能让Shader读取自己上一帧的输出,实现状态的持续传递。
常见避坑点
- 不要用
iTime驱动离散步骤,它是连续值,会导致不同帧的更新次数不一致,用iFrame控制每帧执行一次更新即可。 - 反馈纹理的分辨率不需要太高,2x1或4x4就足够存储两体状态,避免性能浪费。
- 复杂模拟时可以用
highp修饰变量,提升数值精度。
内容的提问来源于stack exchange,提问作者mlseg
相关产品推荐
相关产品推荐

