You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 00:05:12