首页 > 解决方案 > Tile rendering with opengl

问题描述

Let's start by considering this simple snippet:

import ctypes
import textwrap
import time

import glfw
import numpy as np
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

import glm

GLSL_VERSION = "#version 440\n"
CONTEXT_VERSION = (4, 1)


def vs_shader(text):
    return GLSL_VERSION + textwrap.dedent(text)


def shader(text):
    prefix = textwrap.dedent("""\
        uniform float iTime;
        uniform int iFrame;
        uniform vec3 iResolution;
        uniform sampler2D iChannel0;
        uniform vec2 iOffset;
        out vec4 frag_color;
    """)
    suffix = textwrap.dedent("""\
        void main() {
            mainImage(frag_color, gl_FragCoord.xy + iOffset);
        }
    """)

    return GLSL_VERSION + prefix + textwrap.dedent(text) + suffix


VS = vs_shader("""\
    layout(location = 0) in vec3 in_position;

    uniform mat4 mvp;

    void main()
    {
        gl_Position = mvp * vec4(in_position, 1.0f);
    }
""")

SIMPLE = [
    shader("""
        void mainImage( out vec4 fragColor, in vec2 fragCoord )
        {
            vec2 uv = fragCoord.xy / iResolution.xy;
            float tile_size = 4;
            vec2 g = floor(vec2(tile_size, tile_size) * uv);
            float c = mod(g.x + g.y, 2.0);
            if (uv.x<0.5 && uv.y<0.5)
                fragColor = vec4(mix(vec3(c), vec3(1), vec3(1,0,1)), 1.0);
            else if (uv.x>=0.5 && uv.y<0.5)
                fragColor = vec4(mix(vec3(c), vec3(1), vec3(1,0,0)), 1.0);
            else if (uv.x<0.5 && uv.y>=0.5)
                fragColor = vec4(mix(vec3(c), vec3(1), vec3(0,1,0)), 1.0);
            else if (uv.x>=0.5 && uv.y>=0.5)
                fragColor = vec4(mix(vec3(c), vec3(1), vec3(0,0,1)), 1.0);
        }
    """),
    shader("""
        void mainImage( out vec4 fragColor, in vec2 fragCoord )
        {
            vec2 uv = fragCoord/iResolution.xy;
            fragColor = vec4(texture(iChannel0, uv).rgb,1.0);
        }
    """)
]


# -------- MINIFRAMEWORK --------
class Tiler:

    def __init__(self, scene_width, scene_height):
        self.scene_width = scene_width
        self.scene_height = scene_height

    @classmethod
    def from_num_tiles(cls, scene_width, scene_height, num_tiles_x, num_tiles_y):
        obj = cls(scene_width, scene_height)
        obj.num_tiles_x = num_tiles_x
        obj.num_tiles_y = num_tiles_y
        obj.tile_width = obj.scene_width // num_tiles_x
        obj.tile_height = obj.scene_height // num_tiles_y
        return obj

    @classmethod
    def from_size(cls, scene_width, scene_height, tile_width, tile_height):
        obj = cls(scene_width, scene_height)
        obj.num_tiles_x = obj.scene_width // tile_width
        obj.num_tiles_y = obj.scene_height // tile_height
        obj.tile_width = tile_width
        obj.tile_height = tile_height
        return obj

    @property
    def num_tiles(self):
        return self.num_tiles_y * self.num_tiles_x


class TextureF32():

    def __init__(self, width, height):
        target = GL_TEXTURE_2D
        self.target = target
        self.identifier = glGenTextures(1)

        glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
        glBindTexture(target, self.identifier)
        glTexImage2D(target, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, None)
        glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
        glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
        self.set_filter()

        glBindTexture(target, 0)

    def set_filter(self):
        glTexParameteri(self.target, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
        glTexParameteri(self.target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)

    def bind(self):
        glBindTexture(self.target, self.identifier)

    def unbind(self):
        glBindTexture(self.target, 0)


class FboF32():

    def __init__(self, width, height):
        self.target = GL_FRAMEBUFFER
        self.identifier = glGenFramebuffers(1)
        glBindFramebuffer(GL_FRAMEBUFFER, self.identifier)

        # Color attachments
        tex = TextureF32(width, height)
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex.identifier, 0)
        glDrawBuffers(1, [GL_COLOR_ATTACHMENT0])
        self.colors = [tex]

        self.width = width
        self.height = height

        if glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE:
            raise Exception(
                f"ERROR::FRAMEBUFFER:: Framebuffer {self.identifier} is not complete!"
            )

        glBindFramebuffer(GL_FRAMEBUFFER, 0)

    def delete(self):
        self.glDeleteFramebuffers(self.identifier)

    def rect(self):
        return [0, 0, self.width, self.height]

    def bind(self):
        glBindFramebuffer(GL_FRAMEBUFFER, self.identifier)


def set_uniform1f(prog, name, v0):
    glUniform1f(glGetUniformLocation(prog, name), v0)


def set_uniform1i(prog, name, v0):
    glUniform1i(glGetUniformLocation(prog, name), v0)


def set_uniform2i(prog, name, v0, v1):
    glUniform2i(glGetUniformLocation(prog, name), v0, v1)


def set_uniform2f(prog, name, v0, v1):
    glUniform2f(glGetUniformLocation(prog, name), v0, v1)


def set_uniform3f(prog, name, v0, v1, v2):
    glUniform3f(glGetUniformLocation(prog, name), v0, v1, v2)


def set_uniform_mat4(prog, name, mat):
    glUniformMatrix4fv(glGetUniformLocation(prog, name), 1, GL_FALSE, glm.value_ptr(mat))


def set_uniform_texture(prog, name, resource, unit_texture):
    glActiveTexture(GL_TEXTURE0 + unit_texture)
    resource.bind()
    resource.set_filter()
    glUniform1i(glGetUniformLocation(prog, name), 0 + unit_texture)


def create_quad(x0, y0, x1, y1):
    data = np.array([
        x0, y0, 0,
        x1, y0, 0,
        x0, y1, 0,

        x1, y0, 0,
        x1, y1, 0,
        x0, y1, 0,
    ], dtype=np.float32)

    vbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    glBufferData(GL_ARRAY_BUFFER, data, GL_STATIC_DRAW)

    vao = glGenVertexArrays(1)
    glBindVertexArray(vao)
    glVertexAttribPointer(0, 3, GL_FLOAT, False, 0, ctypes.c_void_p(0))
    glEnableVertexAttribArray(0)

    return vao


def compile(shader_type, source):
    identifier = glCreateShader(shader_type)
    glShaderSource(identifier, source)
    glCompileShader(identifier)

    if not glGetShaderiv(identifier, GL_COMPILE_STATUS):
        for i, l in enumerate(source.splitlines()):
            print(f"{i+1}: {l}")
        raise Exception(glGetShaderInfoLog(identifier).decode("utf-8"))

    return identifier


def create_program(vs, fs):
    vs_identifier = compile(GL_VERTEX_SHADER, vs)
    fs_identifier = compile(GL_FRAGMENT_SHADER, fs)

    program = glCreateProgram()
    glAttachShader(program, vs_identifier)
    glAttachShader(program, fs_identifier)
    glLinkProgram(program)
    if not glGetProgramiv(program, GL_LINK_STATUS):
        raise RuntimeError(glGetProgramInfoLog(program))

    return program


# -------- Glut/Glfw --------
class Effect:

    def __init__(self, w, h, num_tiles_x, num_tiles_y, passes):
        self.fbos = []
        self.needs_updating = True
        self.allocations = 0
        self.tiler = Tiler.from_num_tiles(w, h, num_tiles_x, num_tiles_y)

        self.passes = [create_program(VS, rp) for rp in passes]
        self.iframe = 0
        self.start_time = time.time()

        self.quad = create_quad(-1, -1, 1, 1)
        self.view = glm.lookAt(
            glm.vec3(0, 0, 10),
            glm.vec3(0, 0, 0),
            glm.vec3(0, 1, 0)
        )
        self.model = glm.mat4(1)
        glEnable(GL_DEPTH_TEST)

        # print("GL_MAX_VIEWPORT_DIMS:", glGetIntegerv(GL_MAX_VIEWPORT_DIMS))
        # print("GL_MAX_TEXTURE_SIZE:", glGetIntegerv(GL_MAX_TEXTURE_SIZE))
        # print("GL_MAX_RENDERBUFFER_SIZE:", glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE))

    def mem_info(self):
        GL_GPU_MEM_INFO_TOTAL_AVAILABLE_MEM_NVX = 0x9048
        GL_GPU_MEM_INFO_CURRENT_AVAILABLE_MEM_NVX = 0x9049
        total_mem_kb = glGetIntegerv(GL_GPU_MEM_INFO_TOTAL_AVAILABLE_MEM_NVX)
        cur_avail_mem_kb = glGetIntegerv(GL_GPU_MEM_INFO_CURRENT_AVAILABLE_MEM_NVX)
        return f"total_mem_kb={total_mem_kb} cur_avail_mem_kb={cur_avail_mem_kb}"

    def create_fbo(self, tiler):
        return [
            FboF32(width=tiler.tile_width, height=tiler.tile_height)
            for i in range(tiler.num_tiles)
        ]

    def make_ortho(self, x, y, num_tiles_x, num_tiles_y, left, right, bottom, top, near, far):
        # References
        #
        # https://www.opengl.org/archives/resources/code/samples/advanced/advanced97/notes/node20.html
        # https://stackoverflow.com/questions/6490728/capture-snapshot-of-opengl-window-with-very-large-image-resolution
        #
        offset_x = (right - left) / num_tiles_x
        offset_y = (top - bottom) / num_tiles_y
        l = left + offset_x * x
        r = left + offset_x * (x + 1)
        b = bottom + offset_y * y
        t = bottom + offset_y * (y + 1)
        n = near
        f = far
        print(f"x={x} y={y} left={l} right={r} bottom={b} top={t}")
        return glm.ortho(l, r, b, t, n, f)

    def render_pass(self, rp, mvp, w, h, channel0, offset_x=0, offset_y=0):
        t = time.time() - self.start_time

        glBindVertexArray(self.quad)
        glUseProgram(rp)
        set_uniform_mat4(rp, "mvp", mvp)
        set_uniform1f(rp, "iTime", t)
        set_uniform1i(rp, "iFrame", self.iframe)
        set_uniform3f(rp, "iResolution", w, h, w / h)
        set_uniform2f(rp, "iOffset", offset_x, offset_y)
        if channel0:
            set_uniform_texture(rp, "iChannel0", channel0, self.active_texture)
            self.active_texture += 1
        glDrawArrays(GL_TRIANGLES, 0, 6)

    # No tile rendering
    def render_no_tiles(self, window_width, window_height):
        self.active_texture = 0

        if self.needs_updating:
            if not self.fbos:
                print(f"Creating fbos, allocations={self.allocations} {self.mem_info()}")
                self.fbos = [
                    FboF32(width=window_width, height=window_height),
                    FboF32(width=window_width, height=window_height)
                ]

        # clear buffers
        if self.iframe == 0:
            for fbo in self.fbos:
                fbo.bind()
                glViewport(*fbo.rect())
                glClearColor(0, 0, 0, 0)
                glClear(GL_COLOR_BUFFER_BIT)

        proj = glm.ortho(-1, 1, -1, 1, -100, 100)
        mvp = proj * self.view * self.model

        # Pass0: BufferA - Channels [BufferA, None, None, None]
        fbo0 = self.fbos[0]
        fbo1 = self.fbos[1]
        w, h = fbo0.width, fbo0.height
        rp = self.passes[0]
        fbo0.bind()
        glViewport(0, 0, w, h)
        self.render_pass(rp, mvp, w, h, fbo1.colors[0])

        # Pass1: Image - Channels [BufferA, None, None, None]
        glBindFramebuffer(GL_FRAMEBUFFER, 0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        fbo0 = self.fbos[0]
        w, h = window_width, window_height
        rp = self.passes[1]
        glViewport(0, 0, w, h)
        self.render_pass(rp, mvp, w, h, fbo0.colors[0])

        # ping-pong
        self.fbos.reverse()

        self.iframe += 1

    # Tile rendering
    def render_tiles(self, window_width, window_height):
        M = self.tiler.num_tiles_x
        N = self.tiler.num_tiles_y
        offset_x = window_width // M
        offset_y = window_height // N
        proj = glm.ortho(-1, 1, -1, 1, -100, 100)

        # -------- Test --------
        # glBindFramebuffer(GL_FRAMEBUFFER, 0)
        # glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        # self.active_texture = 0

        # for y in range(N):
        #     for x in range(M):
        #         w, h = window_width, window_height
        #         mvp = proj * self.view * self.model
        #         glViewport(offset_x * x, offset_y * y, self.tiler.tile_width, self.tiler.tile_height)
        #         self.render_pass(self.passes[0], mvp, w, h, None, offset_x * x, offset_y * y)

        # return

        # -------- Test2 --------
        self.active_texture = 0

        if self.needs_updating:
            if not self.fbos:
                print(f"Creating fbos, allocations={self.allocations} {self.mem_info()}")
                self.fbos = [
                    self.create_fbo(self.tiler),
                    self.create_fbo(self.tiler),
                ]

        # clear buffers
        if self.iframe == 0:
            for fbo_tiles in self.fbos:
                for fbo in fbo_tiles:
                    fbo.bind()
                    glViewport(*fbo.rect())
                    glClearColor(0, 0, 0, 0)
                    glClear(GL_COLOR_BUFFER_BIT)

        # Pass0: BufferA - Channels [BufferA, None, None, None]
        for y in range(N):
            for x in range(M):
                fbo0 = self.fbos[0][y * M + x]
                fbo1 = self.fbos[1][y * M + x]
                w, h, aspect = fbo0.width, fbo0.height, fbo0.width / fbo0.height
                mvp = proj * self.view * self.model
                rp = self.passes[0]
                fbo0.bind()
                glViewport(0, 0, self.tiler.tile_width, self.tiler.tile_height)
                self.render_pass(rp, mvp, w, h, fbo1.colors[0], offset_x * x, offset_y * y)

        # Pass1: Image - Channels [BufferA, None, None, None]
        glBindFramebuffer(GL_FRAMEBUFFER, 0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        for y in range(N):
            for x in range(M):
                fbo0 = self.fbos[0][y * M + x]
                fbo1 = self.fbos[1][y * M + x]
                w, h, aspect = window_width, window_height, window_width / window_height
                mvp = proj * self.view * self.model
                rp = self.passes[1]
                glViewport(offset_x * x, offset_y * y, self.tiler.tile_width, self.tiler.tile_height)
                self.render_pass(rp, mvp, w, h, fbo0.colors[0], 0, 0)

        # ping-pong
        self.fbos.reverse()

        self.iframe += 1


class WindowGlut:

    def __init__(self, w, h, use_tiles, num_tiles_x, num_tiles_y, passes):
        glutInit()
        glutInitContextVersion(*CONTEXT_VERSION)
        glutInitContextProfile(GLUT_CORE_PROFILE)
        glutInitContextFlags(GLUT_FORWARD_COMPATIBLE)
        glutSetOption(GLUT_MULTISAMPLE, 16)
        glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH | GLUT_MULTISAMPLE)
        glutInitWindowSize(w, h)
        glutCreateWindow('Mcve')
        glutReshapeFunc(self.reshape)
        glutKeyboardFunc(self.keyboard_func)
        glutKeyboardUpFunc(self.keyboard_up_func)
        glutDisplayFunc(self.display)
        glutIdleFunc(self.idle_func)
        self.keys = {chr(i): False for i in range(256)}
        self.effect = Effect(w, h, num_tiles_x, num_tiles_y, passes)

        self.start_time = time.time()
        self.num_frames = 0

        if use_tiles:
            print("TILE RENDERING ENABLED")
            self.render = self.effect.render_tiles
        else:
            print("TILE RENDERING DISABLED")
            self.render = self.effect.render_no_tiles

    def keyboard_func(self, *args):
        self.keys[args[0].decode("utf8")] = True

    def keyboard_up_func(self, *args):
        self.keys[args[0].decode("utf8")] = False

    def display(self):
        if self.keys['r']:
            self.effect.iframe = 0

        self.render(self.window_width, self.window_height)

        glutSwapBuffers()
        self.num_frames += 1

        t = time.time() - self.start_time
        if t >= 1:
            glutSetWindowTitle(f"Fps: {self.num_frames}")
            self.start_time = time.time()
            self.num_frames = 0

    def run(self):
        glutMainLoop()

    def idle_func(self):
        glutPostRedisplay()

    def reshape(self, w, h):
        glViewport(0, 0, w, h)
        self.window_width = w
        self.window_height = h


class WindowGlfw:

    def __init__(self, w, h, use_tiles, num_tiles_x, num_tiles_y, passes):
        # Initialize the library
        if not glfw.init():
            return

        # Create a windowed mode window and its OpenGL context
        glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, CONTEXT_VERSION[0])
        glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, CONTEXT_VERSION[1])
        glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, GL_TRUE)
        glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
        window = glfw.create_window(w, h, "Mcve", None, None)
        if not window:
            glfw.terminate()
            return

        glfw.set_window_size_callback(window, self.reshape)
        glfw.set_key_callback(window, self.keyboard_func)

        # Make the window's context current
        glfw.make_context_current(window)
        self.window = window

        self.keys = {chr(i): False for i in range(256)}
        self.effect = Effect(w, h, num_tiles_x, num_tiles_y, passes)
        self.window_width = w
        self.window_height = h

        if use_tiles:
            print("TILE RENDERING ENABLED")
            self.render = self.effect.render_tiles
        else:
            print("TILE RENDERING DISABLED")
            self.render = self.effect.render_no_tiles

    def keyboard_func(self, window, key, scancode, action, mods):
        self.keys[chr(key)] = action

    def display(self):
        if self.keys['R']:
            self.iframe = 0

        self.render(self.window_width, self.window_height)

    def run(self):
        window = self.window

        while not glfw.window_should_close(window):
            self.display()
            glfw.swap_buffers(window)
            glfw.poll_events()

        glfw.terminate()

    def reshape(self, window, w, h):
        glViewport(0, 0, w, h)
        self.window_width = w
        self.window_height = h


if __name__ == '__main__':
    params = {
        "w": 320,
        "h": 240,
        "use_tiles": True,
        "num_tiles_x": 2,
        "num_tiles_y": 2,
        "passes": SIMPLE
    }
    use_glut = True
    WindowGlut(**params).run() if use_glut else WindowGlfw(**params).run()

To run this code you'll need to install numpy, pyopengl, glfw, PyGLM. You can switch between glfw or glut by toggling the variable use_glut. I've added this options as it seems running glut on macosx may be tricky in certain cases.

Anyway, the goal of this thread is to figure out how to fix the buggy snippet to make proper tile rendering, as you can see right now there is a very naive attempt implemented.

In the main block you can specify if you want to use a render method using tiles or not (use_tiles variable), if you choose using tiles you'll need to specify the number of them (num_tiles_x, num_tiles_y).

Cases:

  1. If you run it with "use_tiles": False you'll see this output:

    enter image description here

    that output is correct

  2. If you run it with "use_tiles": True, "num_tiles_x": 2, "num_tiles_y": 2 you should see the same output than 1). Also correct

  3. But if you run it with "use_tiles": True, "num_tiles_x": 4, "num_tiles_y": 4 or higher you'll start seeing a totally screwed up image like below:

    enter image description here

QUESTION: What's the bug of my tile rendering code that's producing the wrong output? How would you fix it?

Also... Even if the code is fixed the way I'm trying to make tile rendering is quite naive and it won't work very well when dealing with more complex effects where passes need to read back from adjacent tiles or even worst, non-adjacent tiles. For the case of adjacent tiles I've been told adding some padding to the tiles would work pretty well but for the more general case i don't have a clue how'd you tackle that problem. In any case, one step at a time, the goal of this thread would be fixing the buggy snippet

标签: pythonopenglglslrenderingpyopengl

解决方案


在第一遍中,单个图块被渲染到帧缓冲区,该缓冲区具有图块的大小。gl_FragCoord.xy是 (0,0) 在图块的左下角。uv= (0,0) 必须位于窗口的左下方,uv= (1, 1) 必须位于窗口的右上方。要计算uv相对于窗口的坐标,您必须将平铺的偏移量添加到gl_FragCoord.xy并除以窗口的大小:

公式(伪代码):

uv = (gl_FragCoord.xy + (offset_x*x, offset_y*y)) / (window_width, window_height)

    +------------------+
    |                  |
    |    +----+        |
    |    |    |        |
    |    +----+        |
    |  (0,0) tile = gl_FragCoord.xy
    |                  |
    +------------------+
 (0,0) window

在第一遍中,iResolution必须是 ( window_width, window_height) 并且iOffset必须是 ( offset_x * x, offset_y * y)。

# Pass0: BufferA - Channels [BufferA, None, None, None]
for y in range(N):
    for x in range(M):
        fbo0 = self.fbos[0][y * M + x]
        fbo1 = self.fbos[1][y * M + x]
        mvp = proj * self.view * self.model
        rp = self.passes[0]
        fbo0.bind()

        glViewport(0, 0, self.tiler.tile_width, self.tiler.tile_height)

        w, h   = window_width, window_height
        aspect = window_width / window_height
        self.render_pass(rp, mvp, w, h, fbo1.colors[0], offset_x * x, offset_y * y)

在第二遍中,从纹理中读取单个图块并渲染到窗口(默认帧缓冲区 0)。源纹理(瓦片)具有瓦片的大小,并且uv必须相对于瓦片纹理计算坐标。gl_FragCoord.xy是窗口左下角的 (0,0)。uv= (0,0) 必须位于图块的左下角,uv= (1, 1) 必须位于图块的右上角。要计算uv坐标,必须减去瓦片的偏移量,gl_FragCoord.xy结果必须除以标题的大小:

公式(伪代码)

uv = (gl_FragCoord.xy - (offset_x*x, offset_y*y)) / (tile_width, tile_height)

    +------------------+
    |                  |
    |    +----+        |
    |    |    |        |
    |    +----+        |
    |  (0,0) tile      |
    |                  |
    +------------------+
  (0,0) window = gl_FragCoord.xy

在第二遍中,iResolution必须是 ( self.tiler.tile_width, self.tiler.tile_height) 并且iOffset必须是 ( -offset_x * x, -offset_y * y)。

# Pass1: Image - Channels [BufferA, None, None, None]
glBindFramebuffer(GL_FRAMEBUFFER, 0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

for y in range(N):
    for x in range(M):
        fbo0 = self.fbos[0][y * M + x]
        fbo1 = self.fbos[1][y * M + x]
        mvp = proj * self.view * self.model
        rp = self.passes[1]

        glViewport(offset_x*x, offset_y*y, self.tiler.tile_width, self.tiler.tile_height)

        w, h   = self.tiler.tile_width, self.tiler.tile_height
        aspect = self.tiler.tile_width / self.tiler.tile_height
        self.render_pass(rp, mvp, w, h, fbo0.colors[0], -offset_x * x, -offset_y * y)

编辑mcve.py

在这种情况下,渲染目标始终是一个具有图块大小的帧缓冲区。第二个渲染通道(“Pass1”)从一个图块读取并存储到目标图块,因此第二个通道必须是:

# Pass1: Image - Channels [BufferA, None, None, None]
for y in range(N):
    for x in range(M):
        fbo_dst = self.fbo_target[0][y * M + x]
        fbo_src = self.fbos[0][y * M + x]
        mvp = proj * self.view * self.model
        rp = self.passes[1]

        fbo_dst.bind()
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glViewport(0, 0, self.tiler.tile_width, self.tiler.tile_height)

        w, h   = self.tiler.tile_width, self.tiler.tile_height
        aspect = self.tiler.tile_width / self.tiler.tile_height
        self.render_pass(rp, mvp, w, h, fbo_src.colors[0], 0, 0)

另一个问题是从片段着色器中的前一帧读取纹理。纹理的大小始终是图块的大小。纹理的左下坐标为 (0, 0),右上坐标为 (1, 1)。因此,对于纹理坐标 ( st) 的计算,必须跳过偏移量,分辨率由纹理的大小 ( textureSize) 给出:

void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
    initSpheres();

    # issue is here
    // vec2 st = fragCoord.xy / iResolution.xy; <--- delete
    vec2 st = gl_FragCoord.xy / vec2(textureSize(iChannel0, 0));

    // [...]

    // Moving average (multipass code)
    vec3 color = texture(iChannel0, st).rgb * float(iFrame);
    // [...]
}

查看结果:

如果您不想更改 中的着色器代码mainImage,那么另一种方法是欺骗系统并通过宏将纹理查找委托给不同的函数。例如:

def shader(tileTextureLookup, text):

    prefix = textwrap.dedent("""\
        uniform float iTime;
        uniform int iFrame;
        uniform vec3 iResolution;
        uniform sampler2D iChannel0;
        uniform vec2 iOffset;
        out vec4 frag_color;
    """)

    textureLookup = ""
    if tileTextureLookup:
        textureLookup = textwrap.dedent("""\
            vec4 textureTile(sampler2D sampler, vec2 uv) {
                vec2 st = (uv * iResolution.xy - iOffset.xy) / vec2(textureSize(sampler, 0));
                return texture(sampler, st); 
            }

            #define texture textureTile
        """)

    suffix = textwrap.dedent("""\
        void main() {
            mainImage(frag_color, gl_FragCoord.xy + iOffset);
        }
     """)

    return GLSL_VERSION + prefix + textureLookup + textwrap.dedent(text) + suffix
SMALLPT_MULTIPASS = [
    shader(True, """\
        // All code here is by Zavie (https://www.shadertoy.com/view/4sfGDB#)

        // [...]

    """),
    shader(False, """\
        // A simple port of Zavie's GLSL smallpt that uses multipass.
        // Original source: https://www.shadertoy.com/view/4sfGDB#

        void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
            vec2 uv = fragCoord.xy / iResolution.xy;
            vec3 color = texture(iChannel0, uv).rgb;

            fragColor = vec4(pow(clamp(color, 0., 1.), vec3(1./2.2)), 1.);
        }
    """)
]

但请注意,texture它是一个重载函数,这种方法仅适用于二维纹理。此外,还有其他查找功能texelFetch,例如 。


推荐阅读