作为我正在进行的基于PyOpenGL的3D游戏引擎开发的一部分,我试图做一些重构,使使用纹理和帧缓冲区尽可能不痛苦。我觉得这种重构是必要的,这样我的纹理类就可以处理所有这些(还有更多!)用例无缝地:
我创建了一个简单的演示,演示了我的代码。有三个文件:texture.py用于我的texture类(主要关注的是);framebuffer.py用于处理除默认屏幕缓冲区之外的创建框架缓冲区;main.py用于测试这两个类,而不依赖于引擎的其余部分(只将全屏四块呈现到屏幕外的纹理中)。下面是用于覆盖这些用途的最低限度代码:
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)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()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错误。我担心的是:
load_from_data()的名称看,数据可以为null (为了接受传入的框架缓冲区数据)似乎不清楚。该函数是否有一个更好的名称,可以清楚地表明,我可以将数据传递给屏幕,也可以将其保留为空白,以便稍后通过呈现到屏幕外的框架缓冲区来填充数据?此外,在默认的data=None中,如果需要传递数据,甚至不清楚是否需要三维numpy数组。width、height、texture_unit等。如果用户创建了一个纹理,加载了一些数据,然后随后更改了图像的宽度,则数据实际上不会被更新。如何保持这些用户参数和实际的OpenGL数据保持同步?我是否应该对这些纹理参数进行某种验证?我确信,尽管有所有的考虑,还有更多的问题,我可以在后面遇到(例如,“立方体映射”与6个纹理出现在脑海中)。我可以对texture类进行哪些改进,以解决上述所有问题,并使其对于将来的修改具有灵活性?应该将这个texture类分割成多个类来处理,例如,常规呈现与帧缓冲区数据,还是颜色与深度数据?我在网上看到的大多数其他引擎都建立在静态类型的语言之上,方法重载,因此似乎不必担心传入错误的数据或处理Python中缺少的参数。
发布于 2017-02-25 13:29:32
我只想复习一下texture.py。
Texture类的实例代表什么类型?我应该如何创建和使用一个呢?我应该将哪些参数传递给load_from_data方法?get_data方法返回什么?我是否必须按特定的顺序调用这些方法才能正常工作?诸若此类。"RGBA"这样的字符串。这使得静态地检查代码是否正确变得很棘手--像Pylint这样的工具不会发现像"RBGA"这样的打印错误。如果代码使用了枚举,那么检查代码的正确性就更容易了。True表示双线性,False表示最近邻。这种表示方式使得代码很难理解( True代表双线性而不是近邻),这似乎是自找麻烦,因为将来您可能想要支持mipmap,所以GL_TEXTURE_MIN_FILTER需要使用GL_NEAREST_MIPMAP_NEAREST或任何东西。我建议对过滤器使用枚举。Texture类具有load_from_url和load_from_data方法,这些方法表明用户可能希望用新映像重新加载现有的Texture实例。这似乎与Python中通常使用对象的方式相反(如果您想要一个新的纹理,就可以创建一个新的Texture)。所以我会把load_from_url和load_from_data变成类构造函数。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)bind方法必须始终跟随unbind。但是代码并不能确保这种情况的发生--如果bind和unbind之间的代码引发异常,那么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)发布于 2017-02-25 18:39:54
我终于做出了所有这些改变,无论是@GarethRees还是@MathiasEttinger建议的!下面是我更新的texture类:
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切换到了contextlib和bind()解决方案,因为知道以前绑定的纹理单元是很重要的,这样在解除绑定时就可以完全恢复OpenGL状态,如果代码在__enter__和__exit__之间分割,那么很难实现这种状态!我还继续使用属性将构造函数中定义的所有参数从本质上说是只读的,方法是定义没有相应的setter的getter。
为了完整起见,我还包括了新的framebuffer和test源代码:
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)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框架缓冲区和纹理的绑定状态!
https://codereview.stackexchange.com/questions/156260
复制相似问题