首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >游戏引擎的鲁棒纹理和帧缓冲类设计

游戏引擎的鲁棒纹理和帧缓冲类设计
EN

Code Review用户
提问于 2017-02-25 02:24:27
回答 2查看 388关注 0票数 8

作为我正在进行的基于PyOpenGL的3D游戏引擎开发的一部分,我试图做一些重构,使使用纹理和帧缓冲区尽可能不痛苦。我觉得这种重构是必要的,这样我的纹理类就可以处理所有这些(还有更多!)用例无缝地:

  1. 从文件加载图像数据
  2. 在生成的像素数据中加载
  3. 在我的场景中将纹理应用于精灵/模型
  4. 接受各种颜色格式的数据。
  5. 不仅接受颜色数据,而且接受深度和模板数据。
  6. 将框架缓冲区呈现为纹理,以便进行后处理效果/传递。

我创建了一个简单的演示,演示了我的代码。有三个文件:texture.py用于我的texture类(主要关注的是);framebuffer.py用于处理除默认屏幕缓冲区之外的创建框架缓冲区;main.py用于测试这两个类,而不依赖于引擎的其余部分(只将全屏四块呈现到屏幕外的纹理中)。下面是用于覆盖这些用途的最低限度代码:

texture.py

代码语言:javascript
复制
import numpy as np
from OpenGL.GL import *
from OpenGL.GL.ARB.texture_float import *
from OpenGL.GL.EXT.framebuffer_object import *
from PIL import Image

_valid_formats = {
    "RGB": GL_RGB, 
    "RGBA": GL_RGBA, 
    "FLOAT": GL_FLOAT,
    "RGB_FLOAT": GL_RGB32F_ARB,
    "RGBA_FLOAT": GL_RGBA32F_ARB,
}

_valid_filters = [GL_NEAREST, GL_LINEAR]

class Texture(object):

    def __init__(self):
        self._id = glGenTextures(1)

    def get_data(self):
        self.bind()
        raw_data = glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE)
        image = Image.frombytes("RGBA", (self.width, self.height), raw_data)
        image = image.transpose(Image.FLIP_TOP_BOTTOM)
        data = np.array(image)
        self.unbind()
        return data

    def get_id(self):
        return self._id

    def load_from_url(self, asset_url, linear_filter=True, texture_unit=0):
        asset = Image.open(asset_url)
        data = np.array(asset, dtype=np.uint8).flatten()
        width, height = asset.size
        item_size = len(data)/(width * height)
        image_format = "RGBA" if item_size == 4 else "RGB"
        self.load_from_data(width, height, image_format, data, linear_filter, texture_unit)

    def load_from_data(self, width, height, image_format, data=None, linear_filter=True, texture_unit=0):
        self.width = width
        self.height = height
        self.image_format = image_format
        self.linear_filter = linear_filter
        self.texture_unit = texture_unit
        gl_format = _valid_formats[self.image_format]
        self.bind()
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _valid_filters[self.linear_filter])
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _valid_filters[self.linear_filter])
        glTexImage2D(GL_TEXTURE_2D, 0, gl_format, self.width, self.height, 0, gl_format, GL_UNSIGNED_BYTE, data)
        self.unbind()

    def bind(self):
        glActiveTexture(GL_TEXTURE1 + self.texture_unit)
        glBindTexture(GL_TEXTURE_2D, self._id)

    def unbind(self):
        glActiveTexture(GL_TEXTURE0 + self.texture_unit)
        glBindTexture(GL_TEXTURE_2D, 0)
        glActiveTexture(GL_TEXTURE0)

framebuffer.py

代码语言:javascript
复制
import numpy as np
from OpenGL.GL import *
from OpenGL.GL.ARB.texture_float import *
from OpenGL.GL.EXT.framebuffer_object import *
from PIL import Image
from pyorama.graphics.texture import Texture

_valid_attachments = {
    "COLOR_0": GL_COLOR_ATTACHMENT0, 
    "COLOR_1": GL_COLOR_ATTACHMENT1, 
    "COLOR_2": GL_COLOR_ATTACHMENT2, 
    "COLOR_3": GL_COLOR_ATTACHMENT3, 
    "COLOR_4": GL_COLOR_ATTACHMENT4, 
    "COLOR_5": GL_COLOR_ATTACHMENT5, 
    "COLOR_6": GL_COLOR_ATTACHMENT6, 
    "COLOR_7": GL_COLOR_ATTACHMENT7, 
    "COLOR_8": GL_COLOR_ATTACHMENT8, 
    "COLOR_9": GL_COLOR_ATTACHMENT9, 
    "COLOR_10": GL_COLOR_ATTACHMENT10, 
    "COLOR_11": GL_COLOR_ATTACHMENT11, 
    "COLOR_12": GL_COLOR_ATTACHMENT12, 
    "COLOR_13": GL_COLOR_ATTACHMENT13, 
    "COLOR_14": GL_COLOR_ATTACHMENT14, 
    "COLOR_15": GL_COLOR_ATTACHMENT15,
    "DEPTH": GL_DEPTH_ATTACHMENT,
    "STENCIL": GL_STENCIL_ATTACHMENT,
}

class Framebuffer(object):

    def __init__(self):
        self._id = glGenFramebuffersEXT(1)

    def bind(self):
        glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, self._id)

    def attach_texture(self, attachment, texture):
        self.bind()
        glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, _valid_attachments[attachment], GL_TEXTURE_2D, texture.get_id(), 0)
        self.unbind()

    def unbind(self):
        glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0)

    def detach_texture(self, texture):
        self.bind()
        glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, 0, 0)
        self.unbind()

main.py

代码语言:javascript
复制
from pyorama.graphics.texture import Texture
from pyorama.graphics.framebuffer import Framebuffer
import pygame
import sys

width, height = (800, 600)
pygame.init()
pygame.display.set_mode((width, height), pygame.OPENGL | pygame.DOUBLEBUF)
f = Framebuffer()
t = Texture()
t.load_from_data(width, height, "RGBA")
f.attach_texture("COLOR_0", t)

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
    f.bind()
    glClear(GL_COLOR_BUFFER_BIT)
    glClear(GL_DEPTH_BUFFER_BIT)
    glBegin(GL_QUADS)
    glColor(1, 0, 0, 1)
    glVertex(-1, -1)
    glVertex(-1, 1)
    glVertex(1, 1)
    glVertex(1, -1)
    glEnd()
    f.unbind()
    print t.get_data()[0][0]
    pygame.display.flip()

不知怎么的,当这段代码工作的时候,我不禁觉得用户可以通过很多不同的方法破坏纹理类并触发一些神秘的OpenGL错误。我担心的是:

  1. 我试图抽象出许多OpenGL常量。这些常量应该放在模块级别,还是应该放在类级别上呢?我还用一个前导下划线将它们标记为内部。这就是我需要做的,以劝阻用户修改这些常量和破坏行为?
  2. load_from_data()的名称看,数据可以为null (为了接受传入的框架缓冲区数据)似乎不清楚。该函数是否有一个更好的名称,可以清楚地表明,我可以将数据传递给屏幕,也可以将其保留为空白,以便稍后通过呈现到屏幕外的框架缓冲区来填充数据?此外,在默认的data=None中,如果需要传递数据,甚至不清楚是否需要三维numpy数组。
  3. 我有很多可供用户使用的属性,如widthheighttexture_unit等。如果用户创建了一个纹理,加载了一些数据,然后随后更改了图像的宽度,则数据实际上不会被更新。如何保持这些用户参数和实际的OpenGL数据保持同步?我是否应该对这些纹理参数进行某种验证?

我确信,尽管有所有的考虑,还有更多的问题,我可以在后面遇到(例如,“立方体映射”与6个纹理出现在脑海中)。我可以对texture类进行哪些改进,以解决上述所有问题,并使其对于将来的修改具有灵活性?应该将这个texture类分割成多个类来处理,例如,常规呈现与帧缓冲区数据,还是颜色与深度数据?我在网上看到的大多数其他引擎都建立在静态类型的语言之上,方法重载,因此似乎不必担心传入错误的数据或处理Python中缺少的参数。

EN

回答 2

Code Review用户

回答已采纳

发布于 2017-02-25 13:29:32

我只想复习一下texture.py。

  1. 没有任何文件。Texture类的实例代表什么类型?我应该如何创建和使用一个呢?我应该将哪些参数传递给load_from_data方法?get_data方法返回什么?我是否必须按特定的顺序调用这些方法才能正常工作?诸若此类。
  2. 图像格式表示为像"RGBA"这样的字符串。这使得静态地检查代码是否正确变得很棘手--像Pylint这样的工具不会发现像"RBGA"这样的打印错误。如果代码使用了枚举,那么检查代码的正确性就更容易了。
  3. 图像滤波器由布尔表示:True表示双线性,False表示最近邻。这种表示方式使得代码很难理解( True代表双线性而不是近邻),这似乎是自找麻烦,因为将来您可能想要支持mipmap,所以GL_TEXTURE_MIN_FILTER需要使用GL_NEAREST_MIPMAP_NEAREST或任何东西。我建议对过滤器使用枚举。
  4. Texture类具有load_from_urlload_from_data方法,这些方法表明用户可能希望用新映像重新加载现有的Texture实例。这似乎与Python中通常使用对象的方式相反(如果您想要一个新的纹理,就可以创建一个新的Texture)。所以我会把load_from_urlload_from_data变成类构造函数。
  5. Texture类不能确保其实例是有效的。例如,创建一个Texture对象,然后立即调用bind (您得到一个NameError)是一个错误。对于类来说,确保它们的实例是有效的是一个好主意:这使得不小心误用它们变得更加困难。在这种情况下,我会写: def __init__(self,write,height,image_format,data=None,filter=TextureFilter.LINEAR,texture_unit=0):“”. docstring解释参数.“”self._id = glGenTextures(1) self.width =宽度self.height =高度#.等等. @classmethod def from_url(cls,asset_url,filter=TextureFilter.LINEAR,texture_unit=0):“”.在这里实施..。返回cls(宽度、高度、image_format、数据、过滤器、texture_unit)
  6. 看起来,bind方法必须始终跟随unbind。但是代码并不能确保这种情况的发生--如果bindunbind之间的代码引发异常,那么unbind就不会被调用。这看起来像上下文管理器的任务,例如:导入contextlib @contextlib.contextmanager def绑定(Self):“. docstring解释方法.”glActiveTexture(GL_TEXTURE1 + self.texture_unit) glBindTexture(GL_TEXTURE_2D,self._id)试一试:最终产量: glActiveTexture(GL_TEXTURE0 + self.texture_unit) glBindTexture(GL_TEXTURE_2D,0) glActiveTexture(GL_TEXTURE0),然后在load_from_data中你可以写: with self.bind():glTexParameterf(GL_TEXTURE_2D,self.bind,self.bind)( in 20#,GL_TEXTURE_MIN_FILTER,self.filter) glTexImage2D(GL_TEXTURE_2D,0,gl_format,self.width,self.height,0,gl_format,GL_UNSIGNED_BYTE,data)
票数 6
EN

Code Review用户

发布于 2017-02-25 18:39:54

我终于做出了所有这些改变,无论是@GarethRees还是@MathiasEttinger建议的!下面是我更新的texture类:

texture.py

代码语言:javascript
复制
import contextlib
from enum import Enum
import numpy as np
from OpenGL.GL import *
from OpenGL.GL.ARB.texture_float import *
from OpenGL.GL.EXT.framebuffer_object import *
from PIL import Image

class TextureFilter(Enum):
    """Contain the different filter options supported by OpenGL 
    NEAREST: no filtering (use the texel value closest to the pixel of interest's center) - "blocky"
    LINEAR: use a weighted average of the four texels closest to the center of the pixel of interest - default
    """
    NEAREST = GL_NEAREST
    LINEAR = GL_LINEAR

class TextureFormat(Enum):
    """Contain the different internal texture formats supported by OpenGL
    Assumes floats are 32bit, ints are unsigned+8bit
    RGB: Each texel has a red, green, and blue int component (0-255)
    RGBA: Each texel has a red, green, blue, and alpha int component (0-255)
    FLOAT: Each texel has a single float component
    RGB_FLOAT: Each texel has a red, green, and blue float component 
    RGBA_FLOAT: Each texel has a red, green, blue, and alpha float component
    """
    RGB = GL_RGB
    RGBA = GL_RGBA
    FLOAT = GL_FLOAT
    RGB_FLOAT = GL_RGB32F_ARB
    RGBA_FLOAT = GL_RGBA32F_ARB

class Texture(object):

    def __init__(self, width, height, texture_format=TextureFormat.RGBA, data=None, texture_filter=TextureFilter.LINEAR, texture_unit=0):
        """Create a texture object
        width: texture width
        height: texture height
        texture_format: texture format (see TextureFormat)
        data: 1D numpy array containing texture's texel data (use None if the data is to be populated by a framebuffer)
        texture_filter: texture filter type (see TextureFilter)
        texture_unit: the OpenGL texture unit to which the texture should be bound
        Note that all of these properties are read-only (except for data, but only via the update() function)!
        """
        self._texture_id = glGenTextures(1)
        self._width = width
        self._height = height
        self._texture_format = texture_format
        self._texture_filter = texture_filter
        self._texture_unit = texture_unit
        self.update(data)

    @property
    def height(self):
        """Ensure height is a read-only property (no setter defined)"""
        return self._height

    @property
    def texture_filter(self):
        """Ensure texture_filter is a read-only property (no setter defined)"""
        return self._texture_filter

    @property
    def texture_format(self):
        """Ensure texture_format is a read-only property (no setter defined)"""
        return self._texture_format

    @property
    def texture_id(self):
        """Ensure texture_id is a read-only property (no setter defined)"""
        return self._texture_id

    @property
    def texture_unit(self):
        """Ensure texture_unit is a read-only property (no setter defined)"""
        return self._texture_unit

    @property
    def width(self):
        """Ensure width is a read-only property (no setter defined)"""
        return self._width

    def __del__(self):
        """Release the OpenGL texture id and associated GPU memory"""
        glDeleteTextures([self.texture_id])

    @classmethod
    def from_url(cls, asset_url, texture_filter=TextureFilter.LINEAR, texture_unit=0):
        """Create a texture object from an image file
        asset_url: path where image file is located
        texture_filter: texture filter type (see TextureFilter)
        texture_unit: the OpenGL texture unit to which the texture should be bound
        """
        asset = Image.open(asset_url).convert("RGBA")
        data = np.array(asset, dtype=np.uint8).flatten()
        width, height = asset.size
        return cls(width, height, TextureFormat.RGBA, data, texture_filter, texture_unit)

    @contextlib.contextmanager
    def bind(self):
        """Handle binding the texture to and from the correct texture unit
        Revert the OpenGL state when binding is terminated"""
        previous_active = glGetIntegerv(GL_ACTIVE_TEXTURE)
        glActiveTexture(GL_TEXTURE0 + self.texture_unit)
        glBindTexture(GL_TEXTURE_2D, self.texture_id)
        try:
            yield
        finally:
            glActiveTexture(GL_TEXTURE0 + self.texture_unit)
            glBindTexture(GL_TEXTURE_2D, 0)
            glActiveTexture(previous_active)

    def _get_gl_data_type(self):
        """Return the OpenGL data format appropriate for the texture's texture_format (INTERNAL)"""
        if self.texture_format in (TextureFormat.RGB, TextureFormat.RGBA):
            return GL_UNSIGNED_BYTE
        return GL_FLOAT

    def get_texels(self):
        """Retrieve the texel data of the texture from the GPU"""
        texels = None
        data_type = self._get_gl_data_type()
        np_data_type = np.uint8 if data_type == GL_UNSIGNED_BYTE else np.float32
        with self.bind():
            raw_data = glGetTexImage(GL_TEXTURE_2D, 0, self.texture_format.value, data_type)
            texels = np.frombuffer(bytes(raw_data), dtype=np_data_type)
            texels = np.reshape(texels, (self._width, self._height, texels.shape[0]/(self.width * self.height)))
        return texels

    def update(self, data):
        """Update the texture's data and upload the new data to the GPU
        data: new data to replace existing texture data (must match current data numpy array size and dtype)"""
        data_type = self._get_gl_data_type()
        np_data_type = np.uint8 if data_type == GL_UNSIGNED_BYTE else np.float32
        self._data = np.array(data, dtype=np_data_type).flatten() if data is not None else None
        with self.bind():
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, self.texture_filter.value)
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, self.texture_filter.value)
            glTexImage2D(GL_TEXTURE_2D, 0, self.texture_format.value, self.width, self.height, 0, self.texture_format.value, data_type, self._data)

我通过@GarethRees切换到了contextlibbind()解决方案,因为知道以前绑定的纹理单元是很重要的,这样在解除绑定时就可以完全恢复OpenGL状态,如果代码在__enter____exit__之间分割,那么很难实现这种状态!我还继续使用属性将构造函数中定义的所有参数从本质上说是只读的,方法是定义没有相应的setter的getter。

为了完整起见,我还包括了新的framebuffertest源代码:

framebuffer.py

代码语言:javascript
复制
import contextlib
from enum import Enum
import numpy as np
from OpenGL.GL import *
from OpenGL.GL.ARB.texture_float import *
from OpenGL.GL.EXT.framebuffer_object import *
from pyorama.graphics.texture import Texture

class FramebufferAttachment(Enum):
    """Contain the different framebuffer attachment options supported by OpenGL 
    COLOR_#: OpenGL framebuffer color attachment (0-15)
    DEPTH: OpenGL depth attachment
    STENCIL: OpenGL stencil attachment
    """
    COLOR0 = GL_COLOR_ATTACHMENT0 
    COLOR1 = GL_COLOR_ATTACHMENT1 
    COLOR2 = GL_COLOR_ATTACHMENT2 
    COLOR3 = GL_COLOR_ATTACHMENT3 
    COLOR4 = GL_COLOR_ATTACHMENT4 
    COLOR5 = GL_COLOR_ATTACHMENT5 
    COLOR6 = GL_COLOR_ATTACHMENT6 
    COLOR7 = GL_COLOR_ATTACHMENT7 
    COLOR8 = GL_COLOR_ATTACHMENT8 
    COLOR9 = GL_COLOR_ATTACHMENT9 
    COLOR10 = GL_COLOR_ATTACHMENT10 
    COLOR11 = GL_COLOR_ATTACHMENT11 
    COLOR12 = GL_COLOR_ATTACHMENT12 
    COLOR13 = GL_COLOR_ATTACHMENT13 
    COLOR14 = GL_COLOR_ATTACHMENT14 
    COLOR15 = GL_COLOR_ATTACHMENT15
    DEPTH = GL_DEPTH_ATTACHMENT
    STENCIL = GL_STENCIL_ATTACHMENT

class Framebuffer(object):

    def __init__(self):
        """Create a framebuffer object
        Define textures dictionary, where each key:value pair is a texture:attachment
        """
        self._framebuffer_id = glGenFramebuffersEXT(1)
        self._textures = {}

    @property
    def framebuffer_id(self):
        """Ensure framebuffer_id is a read-only property (no setter defined)"""
        return self._framebuffer_id

    @property
    def textures(self):
        """Ensure textures is a read-only property (no setter defined)"""
        return self._textures

    @contextlib.contextmanager
    def bind(self):
        """Handle binding this framebuffer to replace the current framebuffer
        Revert the OpenGL state when binding is terminated
        Framebuffer must have textures attched to it to be bound"""
        previous_active = glGetIntegerv(GL_FRAMEBUFFER_BINDING)
        if self.textures:
            glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, self.framebuffer_id)
        else:
            raise ValueError("Framebuffer has no textures attached to it")
        try:
            yield
        finally:
            glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, previous_active)

    def attach_texture(self, attachment, texture):
        """Attach a texture to receive data from the framebuffer of the desired attachment type
        attachment: what type of framebuffer data the texture is to receive (see FramebufferAttachment)
        texture: the object to attach to this framebuffer
        """
        self.textures[texture] = attachment
        with self.bind():
            glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, attachment.value, GL_TEXTURE_2D, texture.texture_id, 0)

    def detach_texture(self, texture):
        """Detach a texture from this framebuffer to stop sending the texture data
        texture: the object to remove from this framebuffer
        """
        attachment = self.textures.pop(texture)
        with self.bind():
            glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, attachment.value, GL_TEXTURE_2D, 0, 0)

test.py

代码语言:javascript
复制
import numpy as np
import pygame
import sys
from pyorama.graphics.texture import Texture, TextureFilter, TextureFormat
from pyorama.graphics.framebuffer import Framebuffer, FramebufferAttachment
from OpenGL.GL import *

width, height = (800, 600)
pygame.init()
pygame.display.set_mode((width, height), pygame.OPENGL | pygame.DOUBLEBUF)
fbo = Framebuffer()
sprite = Texture.from_url("logo.png")
tex = Texture(width, height, TextureFormat.RGBA)
fbo.attach_texture(FramebufferAttachment.COLOR0, tex)
rotation = 0.0

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            del sprite, tex, fbo
            pygame.quit()
            sys.exit()

    rotation += 0.1
    with fbo.bind():
        glEnable(GL_TEXTURE_2D)
        glEnable(GL_DEPTH_TEST)
        glClear(GL_COLOR_BUFFER_BIT)
        glClear(GL_DEPTH_BUFFER_BIT)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(0, width, 0, height, -1, 1)
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        glTranslate(width/2.0, height/2.0, 0)
        glRotate(rotation, 0, 0, 1)
        glTranslate(-sprite.width/2.0, -sprite.height/2.0, 0)
        with sprite.bind():
            glBegin(GL_QUADS)
            glTexCoord(0, 1)
            glVertex(0, 0)
            glTexCoord(0, 0)
            glVertex(0, sprite.height)
            glTexCoord(1, 0)
            glVertex(sprite.width, sprite.height)
            glTexCoord(1, 1)
            glVertex(sprite.width, 0)
            glEnd()

    glEnable(GL_TEXTURE_2D)
    glClear(GL_COLOR_BUFFER_BIT)
    glClear(GL_DEPTH_BUFFER_BIT)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-1, 1, -1, 1, -1, 1)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    with tex.bind():
        glBegin(GL_QUADS)
        glTexCoord(0, 0)
        glVertex(-1, -1)
        glTexCoord(0, 1)
        glVertex(-1, 1)
        glTexCoord(1, 1)
        glVertex(1, 1)
        glTexCoord(1, 0)
        glVertex(1, -1)
        glEnd()
    pygame.display.flip()

现在使用起来要好得多,特别是因为python的缩进直观地显示了OpenGL框架缓冲区和纹理的绑定状态!

票数 3
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/156260

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档