Homework 0: new shader & 如何使用 ffmpeg 制作 gif

代码就是 https://www.shadertoy.com/new 默认的那个。
主要当成转 gif 的教程,你也可以把以下代码当作改写 shader 的框架代码。

照着改写 Shader 代码比较简单,不过对熟悉 taichi 的语法还是很有意义的。

Shader 框架代码

empty.py

import taichi as ti

# ti.init(debug=True, arch=ti.cpu)
ti.init(arch=ti.gpu)

GUI_TITLE = "Empty"
w, h = wh = (640, 360) # GUI 宽高
pixels = ti.Vector(3, dt=ti.f32, shape=wh)
iResolution = ti.Vector([w, h])

@ti.func
def mainImage(iTime: ti.f32, i: ti.i32, j: ti.i32):
    """ C 源代码
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;

    // Time varying pixel color
    vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));

    // Output to screen
    fragColor = vec4(col,1.0);
    """
    fragCoord = ti.Vector([i, j])

    # Normalized pixel coordinates (from 0 to 1)
    uv = fragCoord / iResolution

    # Time varying pixel color
    uv_xyx = ti.Vector([uv[0], uv[1], uv[0]]) # 暂不支持直接 .xyx
    col = 0.5 + 0.5 * ti.cos(iTime + uv_xyx + ti.Vector([0, 2, 4]))

    # Output to screen
    fragColor = col # 这里 RGB 就够了
    return fragColor

@ti.kernel
def render(t: ti.f32):
    "render 基本不用动,改 mainImage 就可以了"
    for i, j in pixels:
        pixels[i, j] = mainImage(t, i, j)

    return

def main(output_img=False):
    "output_img: 是否输出图片"
    gui = ti.GUI(GUI_TITLE, res=wh)
    for ts in range(1_000_000):
        if gui.get_event(ti.GUI.ESCAPE):
            exit() # 按 ESC 键退出
        
        # render 接受的输入为现实的时间,这里用 ts 计数模拟
        render(ts * 0.03)
        gui.set_image(pixels.to_numpy())
        if output_img:
            # 输出到 frame 文件夹下;4 位顺序命名
            gui.show(f'frame/{ts:04d}.png')
        else:
            gui.show()

if __name__ == '__main__':
    main(output_img=True)
    # main()

制作 GIF

gui.show(f'frame/{ts:04d}.png')
这一句会输出图片到 frame 文件夹,4位数字顺序编号。

然后你会得到一堆图片

这时你需要一个 ffmpeg

我习惯分两步转换:先转成 mp4,再导出成需要的格式,即 gif。
最后删掉原始的图片和转换的结果,只保留 mp4。因为体积小。

转换为 mp4
ffmpeg -framerate 60 -i ./frame/%04d.png -c:v libx264 -r 30 out.mp4

  • 这里的 -framerate 指读取时的帧率,这里输出为 -r 30,所以相当于 60/30=2 倍加速。
    论坛的 gif 大小限制 < 4MB,一定的加速和缩小分辨率有助于减小 gif 大小。
  • -i ./frame/%04d.png 输入文件。
    frame/%04d 和 python 里的统一就行
  • -c:v libx264 x264 编码
  • -r 30 输出 30 fps
  • out.mp4 输出文件名

MP4 转 GIF

ffmpeg -ss 00:00 -t 5 -i out.mp4 -vf "fps=25,scale=320:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 out.gif
  • -ss 00:00 -t 5 从 00:00 开始,剪辑 5 秒
  • -i out.mp4 输入文件
  • -vf -filter:v 的缩写,视频过滤器,后面的字符串是指定的参数
    • fps=30
    • scale=320:-1 等宽高比缩放到宽 320;lanczos 缩放算法
    • split[s0][s1] 将视频分为两个流 s0, s1
      细节参见 FFmpeg Filters Documentation
    • [s0]palettegen[p] 视频流 s0 生成调色板 p
    • [s1][p]paletteuse 视频流 s1 通过调色板 p 进行下采样
  • -loop 0 gif 无限循环;-1 不循环(只播放一遍)
  • out.gif 输出文件

GIF 后处理
ffmpeg 输出的 gif 还能再压缩一下,这里就不折腾 ffmpeg 的参数了。
建议直接去找在线的 gif 压缩工具。

文件大小对比

MP4 (21 s): 390 KB
GIF (7s): 3.38 MB
GIF (在线压缩后): 2.4 MB

成品 GIF

时间点抓得好能得到无缝循环

newShader

6 Likes

我似乎发现了一个bug, 如果把第53行的

gui.set_image(pixels.to_numpy())

改为

gui.set_image(pixels)

就会报错 TypeError: to_numpy() got an unexpected keyword argument ‘as_vector’

根据文档 , set_image是可以接受Vector类型的。

我的环境
python版本: 3.8.3
taichi版本: 0.6.7

感谢大佬分享 :smile:

@comma 确实是 bug

这个用法已经废弃了,但代码中还没改过来

有人已经在 pr 下面指出 bug 了

3 Likes

感谢报告错误,已经修复,将在v0.6.8中发布。

1 Like

谢谢答主!参考了答主的生成方法,我也做了一个gif:
神奇的gif

1 Like

谢谢模板!