//#set OPENGL3 import Foundation import SDL2 #if OPENGL3 import OpenGL #else import OpenGL.GL3 #endif class OpenGL: Renderer { private let srgb = true private var glCtx: SDL_GLContext? = nil private var state = OpenGLState() struct Version { let major, minor: Int32 } let glVersion: Version init(version: Version) { self.glVersion = version } func create(sdlWindow: OpaquePointer) throws { SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, Int32(SDL_GL_CONTEXT_PROFILE_COMPATIBILITY.rawValue)) SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, glVersion.major) SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, glVersion.minor) SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1) SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0) SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 0) if let context = SDL_GL_CreateContext(sdlWindow) { glCtx = context } else { throw RendererError.sdlError(message: "SDL_GL_CreateContext: \(String(cString: SDL_GetError()))") } guard SDL_GL_MakeCurrent(sdlWindow, glCtx) == 0 else { throw RendererError.sdlError(message: "SDL_GL_MakeCurrent: \(String(cString: SDL_GetError()))") } state.enable([.texture2D, .cullFace, .depthTest, .rescaleNormal, .colourMaterial]) if srgb { state.enable(.frameBufferSrgb) } state.cullFace = .back state.clearDepth = 1 state.depthFunc = .less state.setHint(target: .fog, hint: .dontCare) } func delete() { SDL_GL_DeleteContext(glCtx) } func resize(width: Int32, height: Int32) { glViewport(0, 0, width, height) } func newFrame() { glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)) } private var _clearColour = Colour.black var clearColour: Colour { get { _clearColour } set(newColour) { state.clearColour(srgb ? newColour.linear : newColour) _clearColour = newColour } } var wireframe: Bool { get { state.polygonMode == .line } set(mode) { state.polygonMode = mode ? .line : .fill } } func setVsync(mode: VSyncMode) throws { guard SDL_GL_SetSwapInterval(mode.sdlInterval) == 0 else { throw RendererError.sdlError(message: "SDL_GL_SetSwapInterval: \(String(cString: SDL_GetError()))") } } func createMesh(mesh: Mesh) throws -> RenderMesh { var buffers = [GLuint](repeating: 0, count: 2) buffers.withUnsafeMutableBufferPointer { glGenBuffers(2, $0.baseAddress!) } state.arrayBuffer = buffers[0] state.elementArrayBuffer = buffers[1] glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout.stride * mesh.vertices.count, mesh.vertices, GLenum(GL_STATIC_DRAW)) glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER), MemoryLayout.stride * mesh.indices.count, mesh.indices, GLenum(GL_STATIC_DRAW)) state.elementArrayBuffer = OpenGLState.defaultBuffer state.arrayBuffer = OpenGLState.defaultBuffer var subMeshes = [Mesh.SubMesh]() if mesh.subMeshes.isEmpty { subMeshes.append(Mesh.SubMesh(start: 0, length: mesh.indices.count)) } else { for subMesh in mesh.subMeshes { subMeshes.append(Mesh.SubMesh( start: subMesh.value.start, length: subMesh.value.length, material: subMesh.value.material)) } } return RenderMesh( vbo: Int(buffers[0]), ibo: Int(buffers[1]), subMeshes: subMeshes, materials: mesh.materials) } func createTexture(data: UnsafeRawPointer, width: Int, height: Int) throws -> RenderTexture2D { try createTexture(data: data, width: width, height: height, filter: .linear, mipMode: .off) } func createTexture(data: UnsafeRawPointer, width: Int, height: Int, filter: FilterMode, mipMode: MipMode) throws -> RenderTexture2D { let min: Int32 = switch mipMode { case .off: filter.gl case .nearest: filter.glMip case .linear: filter.glLinearMip } return RenderTexture2D(try loadTexture(data: data, width: GLsizei(width), height: GLsizei(height), minFilter: min, magFilter: filter.gl)) } private func loadTexture( data: UnsafeRawPointer, width: GLsizei, height: GLsizei, minFilter: Int32 = GL_LINEAR, magFilter: Int32 = GL_LINEAR, wrap: Int32 = GL_REPEAT) throws -> GLuint { // Create & upload raw bytes as texture var texId: GLuint = 0 glGenTextures(1, &texId) state.bindTexture2D(active: 0, texture: texId) state.texture2DParameter(active: 0, param: .minFilter, int: minFilter) state.texture2DParameter(active: 0, param: .magFilter, int: magFilter) state.texture2DParameter(active: 0, param: .wrapS, int: wrap) state.texture2DParameter(active: 0, param: .wrapT, int: wrap) let format: GLint = srgb ? GL_SRGB8 : GL_RGBA glTexImage2D(GLenum(GL_TEXTURE_2D), 0, format, width, height, 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), data) // Generate mipmaps if needed if [GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST] .contains(minFilter) { glGenerateMipmap(GLenum(GL_TEXTURE_2D)) } state.bindTexture2D(active: 0, texture: OpenGLState.defaultTexture) return texId } func deleteMesh(_ mesh: RenderMesh) { var buffers = [GLuint](repeating: 0, count: 2) buffers[0] = GLuint(mesh.iboHnd) buffers[1] = GLuint(mesh.vboHnd) glDeleteBuffers(1, buffers) } func deleteTexture(_ texture: RenderTexture2D) { var texId = GLuint(texture.id) glDeleteTextures(1, &texId) } func setProjection(matrix: Mat4f) { state.matrixMode = .projection loadMatrix(matrix) } func setView(matrix: Mat4f) { state.matrixMode = .modelView loadMatrix(matrix) } func setMaterial(_ mat: Material) { glColor4f(mat.diffuse.r, mat.diffuse.g, mat.diffuse.b, mat.diffuse.a) state.bindTexture2D(active: 0, texture: mat.texture.isValid ? mat.texture.id : 0) } private func draw(subMesh: Mesh.SubMesh) { glDrawElements( GLenum(GL_TRIANGLES), GLsizei(subMesh.length), GLenum(GL_UNSIGNED_SHORT), .init(bitPattern: MemoryLayout.Index>.stride * subMesh.start)); } private func bindMesh(mesh: RenderMesh) { state.enableClient([ .vertex, .colour, .normal, .textureCoord ]) state.arrayBuffer = UInt32(mesh.vboHnd) state.elementArrayBuffer = UInt32(mesh.iboHnd) let stride = GLsizei(MemoryLayout.stride) if V.self == VertexPositionNormalTexcoord.self { glVertexPointer(3, GLenum(GL_FLOAT), stride, UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalTexcoord.position)!)) glNormalPointer(GLenum(GL_FLOAT), stride, UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalTexcoord.normal)!)) glTexCoordPointer(2, GLenum(GL_FLOAT), stride, UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalTexcoord.texCoord)!)) } else if V.self == VertexPositionNormalColourTexcoord.self { glVertexPointer(3, GLenum(GL_FLOAT), stride, UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.position)!)) glColorPointer(4, GLenum(GL_FLOAT), stride, UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.colour)!)) glNormalPointer(GLenum(GL_FLOAT), stride, UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.normal)!)) glTexCoordPointer(2, GLenum(GL_FLOAT), stride, UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.texCoord)!)) } } private func unbindMesh() { state.elementArrayBuffer = OpenGLState.defaultBuffer state.arrayBuffer = OpenGLState.defaultBuffer state.disableClient([ .vertex, .normal, .textureCoord ]) } private func draw(mesh: RenderMesh) { bindMesh(mesh: mesh) for subMesh in mesh.subMeshes { draw(subMesh: subMesh) } unbindMesh() } func draw(mesh: RenderMesh, model: Mat4f, environment env: Environment) { if (mesh.subMeshes.isEmpty) { return } applyEnvironment(env) state.matrixMode = .modelView glPushMatrix() mulMatrix(model) draw(mesh: mesh) glPopMatrix() } func draw(mesh: RenderMesh, environment env: Environment) { if (mesh.subMeshes.isEmpty) { return } applyEnvironment(env) draw(mesh: mesh) } func draw(mesh: RenderMesh, subMesh: Mesh.SubMesh, environment env: Environment) { applyEnvironment(env) bindMesh(mesh: mesh) draw(subMesh: subMesh) unbindMesh() } private func loadMatrix(_ matrix: Mat4f) { withUnsafePointer(to: matrix.columns) { $0.withMemoryRebound(to: GLfloat.self, capacity: 16) { (values: UnsafePointer) in glLoadMatrixf(values) } } } private func mulMatrix(_ matrix: Mat4f) { withUnsafePointer(to: matrix.columns) { $0.withMemoryRebound(to: GLfloat.self, capacity: 16) { (values: UnsafePointer) in glMultMatrixf(values) } } } private func applyEnvironment(_ env: Environment) { let coordSource = { (mode: Fog.Mode) -> OpenGLState.FogCoordinateSource in switch mode { case .depth: .fragmentDepth case .distance: .coordinate } } switch env.fog { case .none: state.disable(.fog) case .range(let colour, let mode, _, let start, let end): state.fogMode = .linear state.fogColour = srgb ? colour.linear : colour state.fogCoodinateSource = coordSource(mode) state.fogStart = start state.fogEnd = end state.enable(.fog) break case .factor(let colour, let mode, let type, let density): state.fogMode = switch type { case .exp: .exp case .exp2: .exp2 } state.fogColour = srgb ? colour.linear : colour state.fogCoodinateSource = coordSource(mode) state.fogDensity = density state.enable(.fog) break } state.lightModelAmbient = env.ambient let lightCaps: [OpenGLState.Capability] = [ .light0, .light1, .light2, .light3, .light4, .light5, .light6, .light7 ] if !env.lights.isEmpty { state.enable(.lighting) for i in 0..<8 { if i < env.lights.count { switch env.lights[i] { case .directional(let colour, let direction): state.lightPosition[i] = Vec4f(direction, 0) state.lightDiffuse[i] = srgb ? colour.linear : colour case .point(let colour, let position, let intensity): state.lightPosition[i] = Vec4f(position, 1) state.lightDiffuse[i] = srgb ? colour.linear : colour state.lightConstantAttenuation[i] = 0 state.lightLinearAttenuation[i] = intensity state.lightQuadraticAttenuation[i] = 0 } state.enable(lightCaps[i]) } else { state.disable(lightCaps[i]) } } } else { state.disable(.lighting) for cap in lightCaps { state.disable(cap) } } } func drawGizmos(lines: [Line]) { state.disable([ .texture2D, .depthTest ]) var gizmoEnv = Environment() gizmoEnv.fog = .none applyEnvironment(gizmoEnv) glBegin(GLenum(GL_LINES)) for line in lines { let colour = srgb ? line.colour.linear : line.colour glColor4f(colour.r, colour.g, colour.b, colour.a) glVertex3f(line.from.x, line.from.y, line.from.z) glVertex3f(line.to.x, line.to.y, line.to.z) } glEnd() state.enable([ .texture2D, .depthTest ]) } } extension VSyncMode { fileprivate var sdlInterval: Int32 { switch self { case .off: 0 case .on: 1 case .adaptive: -1 } } } extension FilterMode { fileprivate var gl: Int32 { switch self { case .point: GL_NEAREST case .linear: GL_LINEAR } } fileprivate var glMip: Int32 { switch self { case .point: GL_NEAREST_MIPMAP_NEAREST case .linear: GL_LINEAR_MIPMAP_NEAREST } } fileprivate var glLinearMip: Int32 { switch self { case .point: GL_NEAREST_MIPMAP_LINEAR case .linear: GL_LINEAR_MIPMAP_LINEAR } } } extension WrapMode { fileprivate var gl: Int32 { switch self { case .clamping: GL_CLAMP case .clampBorder: GL_CLAMP_TO_BORDER case .clampEdge: GL_CLAMP_TO_EDGE case .mirrored: GL_MIRRORED_REPEAT case .repeating: GL_REPEAT } } }