Source code for pyunity.render

"""
Classes to aid in rendering in a Scene.

"""

__all__ = ["Camera", "Screen", "Shader"]

from typing import Dict
from OpenGL import GL as gl
from ctypes import c_float, c_ubyte, c_void_p
from .values import Color, RGB, Vector3, Vector2, Quaternion, ImmutableStruct
from .errors import PyUnityException
from .core import ShowInInspector, SingleComponent
from .files import Skybox, convert
from . import config, Logger
import glm
import itertools
import os

float_size = gl.sizeof(c_float)

[docs]def gen_buffers(mesh): """ Create buffers for a mesh. Parameters ---------- mesh : Mesh Mesh to create buffers for Returns ------- tuple Tuple containing a vertex buffer object and an index buffer object. """ data = list(itertools.chain(*[[*item[0], *item[1], *item[2]] for item in zip(mesh.verts, mesh.normals, mesh.texcoords)])) indices = list(itertools.chain(*mesh.triangles)) vbo = gl.glGenBuffers(1) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo) gl.glBufferData(gl.GL_ARRAY_BUFFER, len(data) * float_size, convert(c_float, data), gl.GL_STATIC_DRAW) ibo = gl.glGenBuffers(1) gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, ibo) gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, len(indices), convert(c_ubyte, indices), gl.GL_STATIC_DRAW) return vbo, ibo
[docs]def gen_array(): """ Generate a vertex array object. Returns ------- Any A vertex buffer object of floats. Has 3 elements:: # vertex # normal # texcoord x, y, z, a, b, c, u, v """ vao = gl.glGenVertexArrays(1) gl.glBindVertexArray(vao) gl.glVertexAttribPointer( 0, 3, gl.GL_FLOAT, gl.GL_FALSE, 8 * float_size, None) gl.glEnableVertexAttribArray(0) gl.glVertexAttribPointer( 1, 3, gl.GL_FLOAT, gl.GL_FALSE, 8 * float_size, c_void_p(3 * float_size)) gl.glEnableVertexAttribArray(1) gl.glVertexAttribPointer( 2, 2, gl.GL_FLOAT, gl.GL_FALSE, 8 * float_size, c_void_p(6 * float_size)) gl.glEnableVertexAttribArray(2) return vao
[docs]class Shader: def __init__(self, vertex, frag, name): self.vertex = vertex self.frag = frag self.compiled = False self.name = name shaders[name] = self
[docs] def compile(self): """ Compiles shader and generates program. Checks for errors. Notes ===== This function will not work if there is no active framebuffer. """ self.vertexShader = gl.glCreateShader(gl.GL_VERTEX_SHADER) gl.glShaderSource(self.vertexShader, self.vertex, 1, None) gl.glCompileShader(self.vertexShader) success = gl.glGetShaderiv(self.vertexShader, gl.GL_COMPILE_STATUS) if not success: log = gl.glGetShaderInfoLog(self.vertexShader) raise PyUnityException(log) self.fragShader = gl.glCreateShader(gl.GL_FRAGMENT_SHADER) gl.glShaderSource(self.fragShader, self.frag, 1, None) gl.glCompileShader(self.fragShader) success = gl.glGetShaderiv(self.fragShader, gl.GL_COMPILE_STATUS) if not success: log = gl.glGetShaderInfoLog(self.fragShader) raise PyUnityException(log) self.program = gl.glCreateProgram() gl.glAttachShader(self.program, self.vertexShader) gl.glAttachShader(self.program, self.fragShader) gl.glLinkProgram(self.program) success = gl.glGetProgramiv(self.program, gl.GL_LINK_STATUS) if not success: log = gl.glGetProgramInfoLog(self.program) raise PyUnityException(log) gl.glDeleteShader(self.vertexShader) gl.glDeleteShader(self.fragShader) self.compiled = True Logger.LogLine(Logger.INFO, "Compiled shader", repr(self.name))
[docs] @staticmethod def fromFolder(path, name): """ Create a Shader from a folder. It must contain ``vertex.glsl`` and ``fragment.glsl``. Parameters ========== path : str Path of folder to load name : str Name to register this shader to. Used with `Camera.SetShader`. """ if not os.path.isdir(path): raise PyUnityException(f"Folder does not exist: {path!r}") with open(os.path.join(path, "vertex.glsl")) as f: vertex = f.read() with open(os.path.join(path, "fragment.glsl")) as f: fragment = f.read() return Shader(vertex, fragment, name)
[docs] def setVec3(self, var, val): """ Set a ``vec3`` uniform variable. Parameters ========== var : bytes Variable name val : Any Value of uniform variable """ location = gl.glGetUniformLocation(self.program, var) gl.glUniform3f(location, *val)
[docs] def setMat4(self, var, val): """ Set a ``mat4`` uniform variable. Parameters ========== var : bytes Variable name val : Any Value of uniform variable """ location = gl.glGetUniformLocation(self.program, var) gl.glUniformMatrix4fv(location, 1, gl.GL_FALSE, glm.value_ptr(val))
[docs] def setInt(self, var, val): """ Set an ``int`` uniform variable. Parameters ========== var : bytes Variable name val : Any Value of uniform variable """ location = gl.glGetUniformLocation(self.program, var) gl.glUniform1i(location, val)
[docs] def setFloat(self, var, val): """ Set a ``float`` uniform variable. Parameters ========== var : bytes Variable name val : Any Value of uniform variable """ location = gl.glGetUniformLocation(self.program, var) gl.glUniform1f(location, val)
[docs] def use(self): """Compile shader if it isn't compiled, and load it into OpenGL.""" if not self.compiled: self.compile() gl.glUseProgram(self.program)
__dir = os.path.abspath(os.path.dirname(__file__)) shaders: Dict[str, Shader] = dict() skyboxes: Dict[str, Skybox] = dict() skyboxes["Water"] = Skybox(os.path.join( __dir, "shaders", "skybox", "textures")) Shader.fromFolder(os.path.join(__dir, "shaders", "standard"), "Standard") Shader.fromFolder(os.path.join(__dir, "shaders", "skybox"), "Skybox") Shader.fromFolder(os.path.join(__dir, "shaders", "gui"), "GUI")
[docs]def compile_shaders(): for shader in shaders.values(): shader.compile()
[docs]class Camera(SingleComponent): """ Component to hold data about the camera in a scene. Attributes ---------- near : float Distance of the near plane in the camera frustrum. Defaults to 0.05. far : float Distance of the far plane in the camera frustrum. Defaults to 100. clearColor : RGB The clear color of the camera. Defaults to (0, 0, 0). """ near = ShowInInspector(float, 0.05) far = ShowInInspector(float, 200) clearColor = ShowInInspector(Color) shader = ShowInInspector(Shader, shaders["Standard"]) skyboxEnabled = ShowInInspector(bool, True) skybox = ShowInInspector(Skybox, skyboxes["Water"]) ortho = ShowInInspector(bool, False, "Orthographic") def __init__(self, transform): super(Camera, self).__init__(transform) self.size = Screen.size.copy() self.guiShader = shaders["GUI"] self.skyboxShader = shaders["Skybox"] self.clearColor = RGB(0, 0, 0) self.shown["fov"] = ShowInInspector(int, 90, "fov") self.shown["orthoSize"] = ShowInInspector(float, 5, "Ortho Size") self.fov = 90 self.orthoSize = 5 self.viewMat = glm.lookAt([0, 0, 0], [0, 0, -1], [0, 1, 0]) self.lastPos = Vector3.zero() self.lastRot = Quaternion.identity() self.renderPass = False
[docs] def setup_buffers(self): """Creates 2D quad VBO and VAO for GUI.""" data = [ 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, ] self.guiVBO = gl.glGenBuffers(1) self.guiVAO = gl.glGenVertexArrays(1) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.guiVBO) gl.glBufferData(gl.GL_ARRAY_BUFFER, len(data) * float_size, convert(c_float, data), gl.GL_STATIC_DRAW) gl.glBindVertexArray(self.guiVAO) gl.glEnableVertexAttribArray(0) gl.glVertexAttribPointer( 0, 2, gl.GL_FLOAT, gl.GL_FALSE, 2 * float_size, None)
@property def fov(self): """FOV of camera""" return self._fov @fov.setter def fov(self, value): self._fov = value self.projMat = glm.perspective( glm.radians(self._fov / self.size.x * self.size.y), self.size.x / self.size.y, self.near, self.far) @property def orthoSize(self): return self._orthoSize @orthoSize.setter def orthoSize(self, value): self._orthoSize = value width = value * self.size.x / self.size.y self.orthoMat = glm.ortho( -width, width, -value, value, self.near, self.far)
[docs] def Resize(self, width, height): """ Resizes the viewport on screen size change. Parameters ---------- width : int Width of new window height : int Height of new window """ if width == 0 or height == 0: return gl.glViewport(0, 0, width, height) self.size = Vector2(width, height) self.projMat = glm.perspective( glm.radians(self._fov / self.size.x * self.size.y), self.size.x / self.size.y, self.near, self.far) Screen._edit(width, height)
[docs] def getMatrix(self, transform): """Generates model matrix from transform.""" angle, axis = transform.rotation.angleAxisPair angle = glm.radians(angle) axis = axis.normalized() rotated = glm.mat4_cast(glm.angleAxis(angle, list(axis))) position = glm.translate(rotated, list( transform.position * Vector3(1, 1, -1))) scaled = glm.scale(position, list(transform.scale)) return scaled
[docs] def get2DMatrix(self, rectTransform): """Generates model matrix from RectTransform.""" rect = rectTransform.GetRect() + rectTransform.offset rectMin = Vector2.min(rect.min, rect.max) size = (rect.max - rect.min).abs() pivot = size * rectTransform.pivot model = glm.translate(glm.mat4(1), glm.vec3(*(rectMin + pivot), 0)) model = glm.rotate(model, glm.radians( rectTransform.rotation), glm.vec3(0, 0, 1)) model = glm.translate(model, glm.vec3(*-pivot, 0)) model = glm.scale(model, glm.vec3(*(size / 2), 1)) return model
[docs] def getViewMat(self): """Generates view matrix from Transform of camera.""" if self.renderPass and (self.lastPos != self.transform.position or self.lastRot != self.transform.rotation): ## OLD LOOKAT MATRIX GEN ## pos = self.transform.position * Vector3(1, 1, -1) look = pos + \ self.transform.rotation.RotateVector( Vector3.forward()) * Vector3(1, 1, -1) up = self.transform.rotation.RotateVector( Vector3.up()) * Vector3(1, 1, -1) self.viewMat = glm.lookAt(list(pos), list(look), list(up)) # self.viewMat = glm.translate( # glm.mat4_cast(glm.quat(*self.transform.rotation)), # list(self.transform.position * Vector3(-1, -1, 1))) self.lastPos = self.transform.position self.lastRot = self.transform.rotation self.renderPass = False return self.viewMat
[docs] def UseShader(self, name): """Sets current shader from name.""" self.shader = shaders[name]
[docs] def Render(self, renderers, lights): """ Render specific renderers, taking into account light positions. Parameters ========== renderers : List[MeshRenderer] Which meshes to render lights : List[Light] Lights to load into shader """ self.shader.use() viewMat = self.getViewMat() if self.ortho: self.shader.setMat4(b"projection", self.orthoMat) else: self.shader.setMat4(b"projection", self.projMat) self.shader.setMat4(b"view", viewMat) self.shader.setVec3(b"viewPos", list( self.transform.position * Vector3(1, 1, -1))) self.shader.setInt(b"light_num", len(lights)) for i, light in enumerate(lights): lightName = f"lights[{i}].".encode() self.shader.setVec3(lightName + b"pos", light.transform.position * Vector3(1, 1, -1)) self.shader.setFloat(lightName + b"strength", light.intensity * 10) self.shader.setVec3(lightName + b"color", light.color.to_rgb() / 255) self.shader.setInt(lightName + b"type", int(light.type)) self.shader.setVec3(lightName + b"dir", light.transform.rotation.RotateVector(Vector3.forward())) for renderer in renderers: self.shader.setVec3(b"objectColor", renderer.mat.color / 255) self.shader.setMat4( b"model", self.getMatrix(renderer.transform)) if renderer.mat.texture is not None: self.shader.setInt(b"textured", 1) renderer.mat.texture.use() renderer.Render() if self.skyboxEnabled: gl.glDepthFunc(gl.GL_LEQUAL) self.skyboxShader.use() self.skyboxShader.setMat4(b"view", glm.mat4(glm.mat3(viewMat))) self.skyboxShader.setMat4(b"projection", self.projMat) self.skybox.use() gl.glBindVertexArray(self.skybox.vao) gl.glDrawArrays(gl.GL_TRIANGLES, 0, 36) gl.glDepthFunc(gl.GL_LESS)
[docs] def Render2D(self, canvases): """ Render all Image2D and Text components in specified canvases. Parameters ========== canvases : List[Canvas] Canvases to process. All processed GameObjects are cached to prevent duplicate rendering. """ from .gui import Image2D, RectTransform, Text self.guiShader.use() self.guiShader.setMat4( b"projection", glm.ortho(0, *self.size, 0, 10, -10)) gl.glBindVertexArray(self.guiVAO) gl.glDepthMask(gl.GL_FALSE) gameObjects = [] renderers = [] for canvas in canvases: for gameObject in canvas.transform.GetDescendants(): if gameObject in gameObjects: continue gameObjects.append(gameObject) rectTransform = gameObject.GetComponent(RectTransform) if rectTransform is None: continue renderer = gameObject.GetComponent(Image2D) text = gameObject.GetComponent(Text) if renderer is not None: renderers.append((renderer, rectTransform)) if text is not None: renderers.append((text, rectTransform)) if text.texture is None: text.GenTexture() for renderer, rectTransform in renderers: if renderer.texture is None: continue renderer.texture.use() self.guiShader.setMat4( b"model", self.get2DMatrix(rectTransform)) self.guiShader.setFloat(b"depth", renderer.depth) gl.glDrawArrays(gl.GL_QUADS, 0, 4) gl.glDepthMask(gl.GL_TRUE)
[docs]class Screen(metaclass=ImmutableStruct): _names = ["width", "height", "size", "aspect"] width = config.size[0] height = config.size[1] size = Vector2(config.size) aspect = config.size[0] / config.size[1] @classmethod def _edit(cls, width, height): cls._set("width", width) cls._set("height", height) cls._set("size", Vector2(width, height)) cls._set("aspect", width / height)