From 041cfcccaa1e23739d25c2a6c51918ca335c70c8 Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Wed, 15 Jul 2020 21:19:31 -0500 Subject: [PATCH] Added a large portion of I-MIC-FPS's opengl rendering and model loading systems --- assets/textures/default.png | Bin 0 -> 681 bytes cyberarm_engine.gemspec | 2 +- lib/cyberarm_engine.rb | 11 +- lib/cyberarm_engine/model.rb | 215 +++++++++++++ lib/cyberarm_engine/model/material.rb | 20 ++ lib/cyberarm_engine/model/model_object.rb | 128 ++++++++ lib/cyberarm_engine/model/parser.rb | 75 +++++ .../model/parsers/collada_parser.rb | 135 +++++++++ .../model/parsers/wavefront_parser.rb | 156 ++++++++++ lib/cyberarm_engine/model_cache.rb | 35 +++ lib/cyberarm_engine/opengl.rb | 28 ++ lib/cyberarm_engine/opengl/light.rb | 49 +++ .../opengl/orthographic_camera.rb | 42 +++ .../opengl/perspective_camera.rb | 35 +++ .../opengl/renderer/bounding_box_renderer.rb | 248 +++++++++++++++ .../opengl/renderer/g_buffer.rb | 162 ++++++++++ .../opengl/renderer/opengl_renderer.rb | 285 ++++++++++++++++++ .../opengl/renderer/renderer.rb | 22 ++ lib/cyberarm_engine/{ => opengl}/shader.rb | 0 lib/cyberarm_engine/opengl/texture.rb | 67 ++++ lib/cyberarm_engine/transform.rb | 2 +- lib/cyberarm_engine/window.rb | 10 + 22 files changed, 1724 insertions(+), 3 deletions(-) create mode 100644 assets/textures/default.png create mode 100644 lib/cyberarm_engine/model.rb create mode 100644 lib/cyberarm_engine/model/material.rb create mode 100644 lib/cyberarm_engine/model/model_object.rb create mode 100644 lib/cyberarm_engine/model/parser.rb create mode 100644 lib/cyberarm_engine/model/parsers/collada_parser.rb create mode 100644 lib/cyberarm_engine/model/parsers/wavefront_parser.rb create mode 100644 lib/cyberarm_engine/model_cache.rb create mode 100644 lib/cyberarm_engine/opengl.rb create mode 100644 lib/cyberarm_engine/opengl/light.rb create mode 100644 lib/cyberarm_engine/opengl/orthographic_camera.rb create mode 100644 lib/cyberarm_engine/opengl/perspective_camera.rb create mode 100644 lib/cyberarm_engine/opengl/renderer/bounding_box_renderer.rb create mode 100644 lib/cyberarm_engine/opengl/renderer/g_buffer.rb create mode 100644 lib/cyberarm_engine/opengl/renderer/opengl_renderer.rb create mode 100644 lib/cyberarm_engine/opengl/renderer/renderer.rb rename lib/cyberarm_engine/{ => opengl}/shader.rb (100%) create mode 100644 lib/cyberarm_engine/opengl/texture.rb diff --git a/assets/textures/default.png b/assets/textures/default.png new file mode 100644 index 0000000000000000000000000000000000000000..37e027f0f94143a7868873dc1c66d42feb7d6e33 GIT binary patch literal 681 zcmV;a0#^NrP)|*;Fbsedzh0mRVe#VxoPx0n3=E9kIp-9e4gY6}V~7Xc97u-1aLHu}O?IhCa7HS~-MA27y%F=p^%W95`$j2Sh4NOuVV z0Z_S|^aEUcxR`RW6D#MG1jBqVrhBU`tfnTDxeWJ}O@W@G*U&R6=Ov1)o})*~JFIhn zt9{H$kXAqdR4yy5rl?dB#ZJOS0fcQXx4yz`AyCrzR@-F(C^<@W#$!T2@qiMw9~zAX z0VN3z^~+1ReXA{Odczk0M$KbGSiuZp_j(-Ob)W3!B_YfLAjNn9I5ElxQrh*|^aP#p zRd$7`jhC?iHGq`l`0oIOZchzCQ5eq&f}*Nf00e*l5CHzDF1F2u>uHe`-D2gFPKcY 0.15.0" spec.add_dependency "gosu_more_drawables", "~> 0.3" diff --git a/lib/cyberarm_engine.rb b/lib/cyberarm_engine.rb index 43c9250..f33f1c5 100644 --- a/lib/cyberarm_engine.rb +++ b/lib/cyberarm_engine.rb @@ -1,3 +1,5 @@ +CYBERARM_ENGINE_ROOT_PATH = File.expand_path("../..", __FILE__) + begin require File.expand_path("../../ffi-gosu/lib/gosu", File.dirname(__FILE__)) rescue LoadError => e @@ -20,7 +22,6 @@ require_relative "cyberarm_engine/bounding_box" require_relative "cyberarm_engine/vector" require_relative "cyberarm_engine/transform" require_relative "cyberarm_engine/ray" -require_relative "cyberarm_engine/shader" if defined?(OpenGL) require_relative "cyberarm_engine/background" require_relative "cyberarm_engine/animator" @@ -52,3 +53,11 @@ require_relative "cyberarm_engine/ui/elements/slider" require_relative "cyberarm_engine/game_state" require_relative "cyberarm_engine/ui/gui_state" + +require_relative "cyberarm_engine/model" +require_relative "cyberarm_engine/model_cache" +require_relative "cyberarm_engine/model/material" +require_relative "cyberarm_engine/model/model_object" +require_relative "cyberarm_engine/model/parser" +require_relative "cyberarm_engine/model/parsers/wavefront_parser" +require_relative "cyberarm_engine/model/parsers/collada_parser" if defined?(Nokogiri) diff --git a/lib/cyberarm_engine/model.rb b/lib/cyberarm_engine/model.rb new file mode 100644 index 0000000..f038def --- /dev/null +++ b/lib/cyberarm_engine/model.rb @@ -0,0 +1,215 @@ +module CyberarmEngine + class Model + attr_accessor :objects, :materials, :vertices, :uvs, :texures, :normals, :faces, :colors, :bones + attr_accessor :material_file, :current_material, :current_object, :vertex_count, :smoothing + attr_reader :position, :bounding_box, :textured_material, :file_path + + attr_reader :positions_buffer_id, :colors_buffer_id, :normals_buffer_id, :uvs_buffer_id, :textures_buffer_id + attr_reader :vertex_array_id + attr_reader :aabb_tree + + def initialize(file_path:) + @file_path = file_path + + @material_file = nil + @current_object = nil + @current_material=nil + @vertex_count = 0 + + @objects = [] + @materials= {} + @vertices = [] + @colors = [] + @uvs = [] + @normals = [] + @faces = [] + @bones = [] + @smoothing= 0 + + @bounding_box = BoundingBox.new + start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) + + type = File.basename(file_path).split(".").last.to_sym + parser = Model::Parser.find(type) + unless parser + raise "Unsupported model type '.#{type}', supported models are: #{Model::Parser.supported_formats}" + end + + parse(parser) + + @has_texture = false + + @materials.each do |key, material| + if material.texture_id + @has_texture = true + end + end + + allocate_gl_objects + populate_vertex_buffer + configure_vao + + @objects.each {|o| @vertex_count+=o.vertices.size} + + start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) + # build_collision_tree + end + + def parse(parser) + parser.new(self).parse + end + + def calculate_bounding_box(vertices, bounding_box) + unless bounding_box.min.x.is_a?(Float) + vertex = vertices.last + bounding_box.min.x = vertex.x + bounding_box.min.y = vertex.y + bounding_box.min.z = vertex.z + + bounding_box.max.x = vertex.x + bounding_box.max.y = vertex.y + bounding_box.max.z = vertex.z + end + + vertices.each do |vertex| + bounding_box.min.x = vertex.x if vertex.x <= bounding_box.min.x + bounding_box.min.y = vertex.y if vertex.y <= bounding_box.min.y + bounding_box.min.z = vertex.z if vertex.z <= bounding_box.min.z + + bounding_box.max.x = vertex.x if vertex.x >= bounding_box.max.x + bounding_box.max.y = vertex.y if vertex.y >= bounding_box.max.y + bounding_box.max.z = vertex.z if vertex.z >= bounding_box.max.z + end + end + + def allocate_gl_objects + # Allocate arrays for future use + @vertex_array_id = nil + buffer = " " * 4 + glGenVertexArrays(1, buffer) + @vertex_array_id = buffer.unpack('L2').first + + # Allocate buffers for future use + @positions_buffer_id = nil + buffer = " " * 4 + glGenBuffers(1, buffer) + @positions_buffer_id = buffer.unpack('L2').first + + @colors_buffer_id = nil + buffer = " " * 4 + glGenBuffers(1, buffer) + @colors_buffer_id = buffer.unpack('L2').first + + @normals_buffer_id = nil + buffer = " " * 4 + glGenBuffers(1, buffer) + @normals_buffer_id = buffer.unpack('L2').first + + @uvs_buffer_id = nil + buffer = " " * 4 + glGenBuffers(1, buffer) + @uvs_buffer_id = buffer.unpack('L2').first + end + + def populate_vertex_buffer + pos = [] + colors = [] + norms = [] + uvs = [] + + @faces.each do |face| + pos << face.vertices.map { |vert| [vert.x, vert.y, vert.z] } + colors << face.colors.map { |color| [color.red, color.green, color.blue] } + norms << face.normals.map { |vert| [vert.x, vert.y, vert.z, vert.weight] } + + if has_texture? + uvs << face.uvs.map { |vert| [vert.x, vert.y, vert.z] } + end + end + + glBindBuffer(GL_ARRAY_BUFFER, @positions_buffer_id) + glBufferData(GL_ARRAY_BUFFER, pos.flatten.size * Fiddle::SIZEOF_FLOAT, pos.flatten.pack("f*"), GL_STATIC_DRAW) + + glBindBuffer(GL_ARRAY_BUFFER, @colors_buffer_id) + glBufferData(GL_ARRAY_BUFFER, colors.flatten.size * Fiddle::SIZEOF_FLOAT, colors.flatten.pack("f*"), GL_STATIC_DRAW) + + glBindBuffer(GL_ARRAY_BUFFER, @normals_buffer_id) + glBufferData(GL_ARRAY_BUFFER, norms.flatten.size * Fiddle::SIZEOF_FLOAT, norms.flatten.pack("f*"), GL_STATIC_DRAW) + + if has_texture? + glBindBuffer(GL_ARRAY_BUFFER, @uvs_buffer_id) + glBufferData(GL_ARRAY_BUFFER, uvs.flatten.size * Fiddle::SIZEOF_FLOAT, uvs.flatten.pack("f*"), GL_STATIC_DRAW) + end + + glBindBuffer(GL_ARRAY_BUFFER, 0) + end + + def configure_vao + glBindVertexArray(@vertex_array_id) + gl_error? + + # index, size, type, normalized, stride, pointer + # vertices (positions) + glBindBuffer(GL_ARRAY_BUFFER, @positions_buffer_id) + gl_error? + + # inPosition + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nil) + gl_error? + # colors + glBindBuffer(GL_ARRAY_BUFFER, @colors_buffer_id) + # inColor + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, nil) + gl_error? + # normals + glBindBuffer(GL_ARRAY_BUFFER, @normals_buffer_id) + # inNormal + glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, nil) + gl_error? + + if has_texture? + # uvs + glBindBuffer(GL_ARRAY_BUFFER, @uvs_buffer_id) + # inUV + glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 0, nil) + gl_error? + end + + glBindBuffer(GL_ARRAY_BUFFER, 0) + glBindVertexArray(0) + end + + def build_collision_tree + @aabb_tree = AABBTree.new + + @faces.each do |face| + box = BoundingBox.new + box.min = face.vertices.first.dup + box.max = face.vertices.first.dup + + face.vertices.each do |vertex| + if vertex.sum < box.min.sum + box.min = vertex.dup + elsif vertex.sum > box.max.sum + box.max = vertex.dup + end + end + + # FIXME: Handle negatives + box.min *= 1.5 + box.max *= 1.5 + @aabb_tree.insert(face, box) + end + end + + def has_texture? + @has_texture + end + + def release_gl_resources + if @vertex_array_id + + end + end + end +end diff --git a/lib/cyberarm_engine/model/material.rb b/lib/cyberarm_engine/model/material.rb new file mode 100644 index 0000000..1e27cb3 --- /dev/null +++ b/lib/cyberarm_engine/model/material.rb @@ -0,0 +1,20 @@ +module CyberarmEngine + class Model + class Material + attr_accessor :name, :ambient, :diffuse, :specular + attr_reader :texture_id + def initialize(name) + @name = name + @ambient = Color.new(1, 1, 1, 1) + @diffuse = Color.new(1, 1, 1, 1) + @specular= Color.new(1, 1, 1, 1) + @texture = nil + @texture_id = nil + end + + def set_texture(texture_path) + @texture_id = Texture.new(path: texture_path).id + end + end + end +end diff --git a/lib/cyberarm_engine/model/model_object.rb b/lib/cyberarm_engine/model/model_object.rb new file mode 100644 index 0000000..9faf28a --- /dev/null +++ b/lib/cyberarm_engine/model/model_object.rb @@ -0,0 +1,128 @@ +module CyberarmEngine + class Model + class ModelObject + attr_reader :id, :name, :vertices, :uvs, :normals, :materials, :bounding_box, :debug_color + attr_accessor :faces, :scale + + def initialize(id, name) + @id = id + @name = name + @vertices = [] + @uvs = [] + @normals = [] + @faces = [] + @materials = [] + @bounding_box = BoundingBox.new + @debug_color = Color.new(1.0,1.0,1.0) + + @scale = 1.0 + + # Faces array packs everything: + # vertex = index[0] + # uv = index[1] + # normal = index[2] + # material = index[3] + end + + def has_texture? + @materials.find { |mat| mat.texture_id } ? true : false + end + + def reflatten + @vertices_list = nil + @uvs_list = nil + @normals_list = nil + + flattened_vertices + flattened_uvs + flattened_normals + end + + def flattened_vertices + unless @vertices_list + @debug_color = @faces.first.material.diffuse + + list = [] + @faces.each do |face| + face.vertices.each do |v| + next unless v + list << v.x*@scale + list << v.y*@scale + list << v.z*@scale + list << v.weight + end + end + + @vertices_list_size = list.size + @vertices_list = list.pack("f*") + end + + return @vertices_list + end + + def flattened_vertices_size + @vertices_list_size + end + + def flattened_uvs + unless @uvs_list + list = [] + @faces.each do |face| + face.uvs.each do |v| + next unless v + list << v.x + list << v.y + list << v.z + end + end + + @uvs_list_size = list.size + @uvs_list = list.pack("f*") + end + + return @uvs_list + end + + def flattened_normals + unless @normals_list + list = [] + @faces.each do |face| + face.normals.each do |n| + next unless n + list << n.x + list << n.y + list << n.z + end + end + + @normals_list_size = list.size + @normals_list = list.pack("f*") + end + + return @normals_list + end + + def flattened_materials + unless @materials_list + list = [] + @faces.each do |face| + material = face.material + next unless material + face.vertices.each do # Add material to each vertex + + list << material.diffuse.red + list << material.diffuse.green + list << material.diffuse.blue + # list << material.alpha + end + end + + @materials_list_size = list.size + @materials_list = list.pack("f*") + end + + return @materials_list + end + end + end +end diff --git a/lib/cyberarm_engine/model/parser.rb b/lib/cyberarm_engine/model/parser.rb new file mode 100644 index 0000000..1d9ba89 --- /dev/null +++ b/lib/cyberarm_engine/model/parser.rb @@ -0,0 +1,75 @@ +module CyberarmEngine + TextureCoordinate = Struct.new(:u, :v, :weight) + Point = Struct.new(:x, :y) + Color = Struct.new(:red, :green, :blue, :alpha) + Face = Struct.new(:vertices, :uvs, :normals, :colors, :material, :smoothing) + + class Model + class Parser + @@parsers = [] + + def self.handles + raise NotImplementedError, "Model::Parser#handles must return an array of file extensions that this parser supports" + end + + def self.inherited(parser) + @@parsers << parser + end + + def self.find(file_type) + found_parser = @@parsers.find do |parser| + parser.handles.include?(file_type) + end + + return found_parser + end + + def self.supported_formats + @@parsers.map { |parser| parser.handles }.flatten.map { |s| ".#{s}" }.join(", ") + end + + def initialize(model) + @model = model + end + + def parse + end + + def set_object(id: nil, name: nil) + _model = nil + + if id + _model = @model.objects.find { |o| o.id == id } + elsif name + _model = @model.objects.find { |o| o.name == name } + else + raise "Must provide either an id: or name:" + end + + if _model + @model.current_object = _model + else + raise "Couldn't find ModelObject!" + end + end + + def change_object(id, name) + @model.objects << Model::ModelObject.new(id, name) + @model.current_object = @model.objects.last + end + + def set_material(name) + @model.current_material = name + @model.current_object.materials << current_material + end + + def add_material(name, material) + @model.materials[name] = material + end + + def current_material + @model.materials[@model.current_material] + end + end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/model/parsers/collada_parser.rb b/lib/cyberarm_engine/model/parsers/collada_parser.rb new file mode 100644 index 0000000..18064e6 --- /dev/null +++ b/lib/cyberarm_engine/model/parsers/collada_parser.rb @@ -0,0 +1,135 @@ +module CyberarmEngine + class ColladaParser < Model::Parser + def self.handles + [:dae] + end + + def parse + @collada = Nokogiri::XML(File.read(@model.file_path)) + + @collada.css("library_materials material").each do |material| + parse_material(material) + end + + @collada.css("library_geometries geometry").each do |geometry| + parse_geometry(geometry) + end + + @model.calculate_bounding_box(@model.vertices, @model.bounding_box) + @model.objects.each do |o| + @model.calculate_bounding_box(o.vertices, o.bounding_box) + end + end + + def parse_material(material) + name = material.attributes["id"].value + effect_id = material.at_css("instance_effect").attributes["url"].value + + mat = Model::Material.new(name) + effect = @collada.at_css("[id=\"#{effect_id.sub('#', '')}\"]") + + emission = effect.at_css("emission color") + diffuse = effect.at_css("diffuse color").children.first.to_s.split(" ").map { |c| Float(c) } + + mat.diffuse = Color.new(*diffuse[0..2]) + + add_material(name, mat) + end + + def parse_geometry(geometry) + geometry_id = geometry.attributes["id"].value + geometry_name = geometry.attributes["name"].value + + change_object(geometry_id, geometry_name) + + mesh = geometry.at_css("mesh") + + get_positions(geometry_id, mesh) + get_normals(geometry_id, mesh) + get_texture_coordinates(geometry_id, mesh) + + project_node(geometry_name) + build_faces(geometry_id, mesh) + end + + def get_positions(id, mesh) + positions = mesh.at_css("[id=\"#{id}-positions\"]") + array = positions.at_css("[id=\"#{id}-positions-array\"]") + + stride = Integer(positions.at_css("[source=\"##{id}-positions-array\"]").attributes["stride"].value) + list = array.children.first.to_s.split(" ").map{ |f| Float(f) }.each_slice(stride).each do |slice| + position = Vector.new(*slice) + @model.current_object.vertices << position + @model.vertices << position + end + end + + def get_normals(id, mesh) + normals = mesh.at_css("[id=\"#{id}-normals\"]") + array = normals.at_css("[id=\"#{id}-normals-array\"]") + + stride = Integer(normals.at_css("[source=\"##{id}-normals-array\"]").attributes["stride"].value) + list = array.children.first.to_s.split(" ").map{ |f| Float(f) }.each_slice(stride).each do |slice| + normal = Vector.new(*slice) + @model.current_object.normals << normal + @model.normals << normal + end + end + + def get_texture_coordinates(id, mesh) + end + + def project_node(name) + @collada.css("library_visual_scenes visual_scene node").each do |node| + if node.attributes["name"].value == name + transform = Transform.new( node.at_css("matrix").children.first.to_s.split(" ").map { |f| Float(f) } ) + + @model.current_object.vertices.each do |vert| + v = vert.multiply_transform(transform) + vert.x, vert.y, vert.z, vert.w = v.x, v.y, v.z, v.w + end + + break + end + end + end + + def build_faces(id, mesh) + material_name = mesh.at_css("triangles").attributes["material"].value + set_material(material_name) + + positions_index = [] + normals_index = [] + uvs_index = [] + + mesh.at_css("triangles p").children.first.to_s.split(" ").map { |i| Integer(i) }.each_slice(3).each do |slice| + positions_index << slice[0] + normals_index << slice[1] + uvs_index << slice[2] + end + + norm_index = 0 + positions_index.each_slice(3) do |slice| + face = Face.new + face.vertices = [] + face.uvs = [] + face.normals = [] + face.colors = [] + face.material = current_material + face.smoothing= @model.smoothing + + slice.each do |index| + face.vertices << @model.vertices[index] + # face.uvs << @model.uvs[index] + face.normals << @model.normals[normals_index[norm_index]] + face.colors << current_material.diffuse + + norm_index += 1 + end + + @model.current_object.faces << face + @model.faces << face + end + end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/model/parsers/wavefront_parser.rb b/lib/cyberarm_engine/model/parsers/wavefront_parser.rb new file mode 100644 index 0000000..274bee8 --- /dev/null +++ b/lib/cyberarm_engine/model/parsers/wavefront_parser.rb @@ -0,0 +1,156 @@ +module CyberarmEngine + class WavefrontParser < Model::Parser + def self.handles + [:obj] + end + + def parse + lines = 0 + list = File.read(@model.file_path).split("\n") + list.each do |line| + lines+=1 + line = line.strip + + array = line.split(' ') + case array[0] + when 'mtllib' + @model.material_file = array[1] + parse_mtllib + when 'usemtl' + set_material(array[1]) + when 'o' + change_object(nil, array[1]) + when 's' + set_smoothing(array[1]) + when 'v' + add_vertex(array) + when 'vt' + add_texture_coordinate(array) + + when 'vn' + add_normal(array) + + when 'f' + verts = [] + uvs = [] + norms = [] + array[1..3].each do |f| + verts << f.split("/")[0] + uvs << f.split("/")[1] + norms << f.split("/")[2] + end + + face = Face.new + face.vertices = [] + face.uvs = [] + face.normals = [] + face.colors = [] + face.material = current_material + face.smoothing= @model.smoothing + + mat = face.material.diffuse + color = mat + + verts.each_with_index do |v, index| + + if uvs.first != "" + face.vertices << @model.vertices[Integer(v)-1] + face.uvs << @model.uvs[Integer(uvs[index])-1] + face.normals << @model.normals[Integer(norms[index])-1] + face.colors << color + else + face.vertices << @model.vertices[Integer(v)-1] + face.uvs << nil + face.normals << @model.normals[Integer(norms[index])-1] + face.colors << color + end + end + + @model.current_object.faces << face + @model.faces << face + end + end + + @model.calculate_bounding_box(@model.vertices, @model.bounding_box) + @model.objects.each do |o| + @model.calculate_bounding_box(o.vertices, o.bounding_box) + end + end + + def parse_mtllib + file = File.open(@model.file_path.sub(File.basename(@model.file_path), '')+@model.material_file, 'r') + file.readlines.each do |line| + array = line.strip.split(' ') + case array.first + when 'newmtl' + material = Model::Material.new(array.last) + @model.current_material = array.last + @model.materials[array.last] = material + when 'Ns' # Specular Exponent + when 'Ka' # Ambient color + @model.materials[@model.current_material].ambient = Color.new(Float(array[1]), Float(array[2]), Float(array[3])) + when 'Kd' # Diffuse color + @model.materials[@model.current_material].diffuse = Color.new(Float(array[1]), Float(array[2]), Float(array[3])) + when 'Ks' # Specular color + @model.materials[@model.current_material].specular = Color.new(Float(array[1]), Float(array[2]), Float(array[3])) + when 'Ke' # Emissive + when 'Ni' # Unknown (Blender Specific?) + when 'd' # Dissolved (Transparency) + when 'illum' # Illumination model + when 'map_Kd' # Diffuse texture + texture = File.basename(array[1]) + texture_path = "#{File.expand_path("../../", @model.file_path)}/textures/#{texture}" + @model.materials[@model.current_material].set_texture(texture_path) + end + end + end + + def set_smoothing(value) + if value == "1" + @model.smoothing = true + else + @model.smoothing = false + end + end + + def add_vertex(array) + @model.vertex_count+=1 + vert = nil + if array.size == 5 + vert = Vector.new(Float(array[1]), Float(array[2]), Float(array[3]), Float(array[4])) + elsif array.size == 4 + vert = Vector.new(Float(array[1]), Float(array[2]), Float(array[3]), 1.0) + else + raise + end + @model.current_object.vertices << vert + @model.vertices << vert + end + + def add_normal(array) + vert = nil + if array.size == 5 + vert = Vector.new(Float(array[1]), Float(array[2]), Float(array[3]), Float(array[4])) + elsif array.size == 4 + vert = Vector.new(Float(array[1]), Float(array[2]), Float(array[3]), 1.0) + else + raise + end + @model.current_object.normals << vert + @model.normals << vert + end + + def add_texture_coordinate(array) + texture = nil + if array.size == 4 + texture = Vector.new(Float(array[1]), 1-Float(array[2]), Float(array[3])) + elsif array.size == 3 + texture = Vector.new(Float(array[1]), 1-Float(array[2]), 1.0) + else + raise + end + @model.uvs << texture + @model.current_object.uvs << texture + end + end +end diff --git a/lib/cyberarm_engine/model_cache.rb b/lib/cyberarm_engine/model_cache.rb new file mode 100644 index 0000000..ca123a3 --- /dev/null +++ b/lib/cyberarm_engine/model_cache.rb @@ -0,0 +1,35 @@ +module CyberarmEngine + module ModelCache + CACHE = {} + + def self.find_or_cache(manifest:) + model_file = manifest.file_path + "/model/#{manifest.model}" + + type = File.basename(model_file).split(".").last.to_sym + + if model = load_model_from_cache(type, model_file) + return model + else + model = CyberarmEngine::Model.new(file_path: model_file) + cache_model(type, model_file, model) + + return model + end + end + + def self.load_model_from_cache(type, model_file) + if CACHE[type].is_a?(Hash) + if CACHE[type][model_file] + return CACHE[type][model_file] + end + end + + return false + end + + def self.cache_model(type, model_file, model) + CACHE[type] = {} unless CACHE[type].is_a?(Hash) + CACHE[type][model_file] = model + end + end +end diff --git a/lib/cyberarm_engine/opengl.rb b/lib/cyberarm_engine/opengl.rb new file mode 100644 index 0000000..3eb17f5 --- /dev/null +++ b/lib/cyberarm_engine/opengl.rb @@ -0,0 +1,28 @@ +begin + require "opengl" +rescue LoadError + puts "Required gem is not installed, please install 'opengl-bindings' and try again." + exit(1) +end + +module CyberarmEngine + def gl_error? + e = glGetError() + if e != GL_NO_ERROR + $stderr.puts "OpenGL error detected by handler at: #{caller[0]}" + $stderr.puts " #{gluErrorString(e)} (#{e})\n" + exit if window.exit_on_opengl_error? + end + end +end + +require_relative "opengl/shader" +require_relative "opengl/texture" +require_relative "opengl/light" +require_relative "opengl/perspective_camera" +require_relative "opengl/orthographic_camera" + +require_relative "opengl/renderer/g_buffer" +require_relative "opengl/renderer/bounding_box_renderer" +require_relative "opengl/renderer/opengl_renderer" +require_relative "opengl/renderer/renderer" \ No newline at end of file diff --git a/lib/cyberarm_engine/opengl/light.rb b/lib/cyberarm_engine/opengl/light.rb new file mode 100644 index 0000000..8301567 --- /dev/null +++ b/lib/cyberarm_engine/opengl/light.rb @@ -0,0 +1,49 @@ +module CyberarmEngine + class Light + DIRECTIONAL = 0 + POINT = 1 + SPOT = 2 + + attr_reader :light_id + attr_accessor :type, :ambient, :diffuse, :specular, :position, :direction, :intensity + def initialize( + id:, + type: Light::POINT, + ambient: Vector.new(0.5, 0.5, 0.5), + diffuse: Vector.new(1, 1, 1), + specular: Vector.new(0.2, 0.2, 0.2), + position: Vector.new(0, 0, 0), + direction: Vector.new(0, 0, 0), + intensity: 1 + ) + @light_id = id + @type = type + + @ambient = ambient + @diffuse = diffuse + @specular = specular + @position = position + @direction = direction + + @intensity = intensity + end + + def draw + glLightfv(@light_id, GL_AMBIENT, convert(@ambient).pack("f*")) + glLightfv(@light_id, GL_DIFFUSE, convert(@diffuse, true).pack("f*")) + glLightfv(@light_id, GL_SPECULAR, convert(@specular, true).pack("f*")) + glLightfv(@light_id, GL_POSITION, convert(@position).pack("f*")) + glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, 1) + glEnable(GL_LIGHTING) + glEnable(@light_id) + end + + def convert(struct, apply_intensity = false) + if apply_intensity + return struct.to_a.compact.map{ |i| i * @intensity } + else + return struct.to_a.compact + end + end + end +end diff --git a/lib/cyberarm_engine/opengl/orthographic_camera.rb b/lib/cyberarm_engine/opengl/orthographic_camera.rb new file mode 100644 index 0000000..2df57a6 --- /dev/null +++ b/lib/cyberarm_engine/opengl/orthographic_camera.rb @@ -0,0 +1,42 @@ +module CyberarmEngine + class OrthographicCamera + attr_accessor :position, :orientation, :zoom, :left, :right, :bottom, :top, + :min_view_distance, :max_view_distance + def initialize( + position:, orientation: Vector.new(0, 0, 0), + zoom: 1, left: 0, right:, bottom: 0, top:, + min_view_distance: 0.1, max_view_distance: 200.0 + ) + @position = position + @orientation = orientation + + @zoom = zoom + + @left, @right, @bottom, @top = left, right, bottom, top + + @min_view_distance = min_view_distance + @max_view_distance = max_view_distance + end + + # Immediate mode renderering fallback + def draw + glMatrixMode(GL_PROJECTION) + glLoadIdentity + glOrtho(@left, @right, @bottom, @top, @min_view_distance, @max_view_distance) + glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST) + glRotatef(@orientation.x, 1, 0, 0) + glRotatef(@orientation.y, 0, 1, 0) + glTranslatef(-@position.x, -@position.y, -@position.z) + glMatrixMode(GL_MODELVIEW) + glLoadIdentity + end + + def projection_matrix + Transform.orthographic(@left, @right, @bottom, @top, @min_view_distance, @max_view_distance) + end + + def view_matrix + Transform.translate_3d(@position * -1) * Transform.rotate_3d(@orientation) + end + end +end diff --git a/lib/cyberarm_engine/opengl/perspective_camera.rb b/lib/cyberarm_engine/opengl/perspective_camera.rb new file mode 100644 index 0000000..50b0909 --- /dev/null +++ b/lib/cyberarm_engine/opengl/perspective_camera.rb @@ -0,0 +1,35 @@ +module CyberarmEngine + class PerspectiveCamera + attr_accessor :position, :orientation, :aspect_ratio, :field_of_view + def initialize(position:, orientation: Vector.new(0, 0, 0), aspect_ratio:, field_of_view: 70.0, min_view_distance: 0.1, max_view_distance: 155.0) + @position = position + @orientation = orientation + + @aspect_ratio = aspect_ratio + @field_of_view = field_of_view + + @min_view_distance = min_view_distance + @max_view_distance = max_view_distance + end + + def draw + glMatrixMode(GL_PROJECTION) + glLoadIdentity + gluPerspective(@field_of_view, @aspect_ratio, @min_view_distance, @max_view_distance) + glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST) + glRotatef(@orientation.x, 1, 0, 0) + glRotatef(@orientation.y, 0, 1, 0) + glTranslatef(-@position.x, -@position.y, -@position.z) + glMatrixMode(GL_MODELVIEW) + glLoadIdentity + end + + def projection_matrix + Transform.perspective(@field_of_view, @aspect_ratio, @min_view_distance, @max_view_distance) + end + + def view_matrix + Transform.translate_3d(@position * -1) * Transform.rotate_3d(@orientation) + end + end +end diff --git a/lib/cyberarm_engine/opengl/renderer/bounding_box_renderer.rb b/lib/cyberarm_engine/opengl/renderer/bounding_box_renderer.rb new file mode 100644 index 0000000..3048288 --- /dev/null +++ b/lib/cyberarm_engine/opengl/renderer/bounding_box_renderer.rb @@ -0,0 +1,248 @@ +module CyberarmEngine + class BoundingBoxRenderer + attr_reader :bounding_boxes, :vertex_count + def initialize + @bounding_boxes = {} + @vertex_count = 0 + end + + def render(entities) + entities.each do |entity| + create_bounding_box(entity,color = nil) + draw_bounding_boxes + end + + (@bounding_boxes.keys - entities.map { |e| e.object_id }).each do |key| + @bounding_boxes.delete(key) + end + end + + def create_bounding_box(entity, color = nil) + color ||= entity.debug_color + entity_id = entity.object_id + + if @bounding_boxes[entity_id] + if @bounding_boxes[entity_id][:color] != color + @bounding_boxes[entity_id][:colors] = mesh_colors(color).pack("f*") + @bounding_boxes[entity_id][:color] = color + return + else + return + end + end + + @bounding_boxes[entity_id] = { + entity: entity, + color: color, + objects: [] + } + + box = entity.normalize_bounding_box + + normals = mesh_normals + colors = mesh_colors(color) + vertices = mesh_vertices(box) + + @vertex_count+=vertices.size + + @bounding_boxes[entity_id][:vertices_size] = vertices.size + @bounding_boxes[entity_id][:vertices] = vertices.pack("f*") + @bounding_boxes[entity_id][:normals] = normals.pack("f*") + @bounding_boxes[entity_id][:colors] = colors.pack("f*") + + entity.model.objects.each do |mesh| + data = {} + box = mesh.bounding_box.normalize(entity) + + normals = mesh_normals + colors = mesh_colors(mesh.debug_color) + vertices = mesh_vertices(box) + + @vertex_count+=vertices.size + + data[:vertices_size] = vertices.size + data[:vertices] = vertices.pack("f*") + data[:normals] = normals.pack("f*") + data[:colors] = colors.pack("f*") + + @bounding_boxes[entity_id][:objects] << data + end + end + + def mesh_normals + [ + 0,1,0, + 0,1,0, + 0,1,0, + 0,1,0, + 0,1,0, + 0,1,0, + + 0,-1,0, + 0,-1,0, + 0,-1,0, + 0,-1,0, + 0,-1,0, + 0,-1,0, + + 0,0,1, + 0,0,1, + 0,0,1, + 0,0,1, + 0,0,1, + 0,0,1, + + 1,0,0, + 1,0,0, + 1,0,0, + 1,0,0, + 1,0,0, + 1,0,0, + + -1,0,0, + -1,0,0, + -1,0,0, + -1,0,0, + -1,0,0, + -1,0,0, + + -1,0,0, + -1,0,0, + -1,0,0, + + -1,0,0, + -1,0,0, + -1,0,0 + ] + end + + def mesh_colors(color) + [ + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue, + color.red, color.green, color.blue + ] + end + + def mesh_vertices(box) + [ + box.min.x, box.max.y, box.max.z, + box.min.x, box.max.y, box.min.z, + box.max.x, box.max.y, box.min.z, + + box.min.x, box.max.y, box.max.z, + box.max.x, box.max.y, box.max.z, + box.max.x, box.max.y, box.min.z, + + box.max.x, box.min.y, box.min.z, + box.max.x, box.min.y, box.max.z, + box.min.x, box.min.y, box.max.z, + + box.max.x, box.min.y, box.min.z, + box.min.x, box.min.y, box.min.z, + box.min.x, box.min.y, box.max.z, + + box.min.x, box.max.y, box.max.z, + box.min.x, box.max.y, box.min.z, + box.min.x, box.min.y, box.min.z, + + box.min.x, box.min.y, box.max.z, + box.min.x, box.min.y, box.min.z, + box.min.x, box.max.y, box.max.z, + + box.max.x, box.max.y, box.max.z, + box.max.x, box.max.y, box.min.z, + box.max.x, box.min.y, box.min.z, + + box.max.x, box.min.y, box.max.z, + box.max.x, box.min.y, box.min.z, + box.max.x, box.max.y, box.max.z, + + box.min.x, box.max.y, box.max.z, + box.max.x, box.max.y, box.max.z, + box.max.x, box.min.y, box.max.z, + + box.min.x, box.max.y, box.max.z, + box.max.x, box.min.y, box.max.z, + box.min.x, box.min.y, box.max.z, + + box.max.x, box.min.y, box.min.z, + box.min.x, box.min.y, box.min.z, + box.min.x, box.max.y, box.min.z, + + box.max.x, box.min.y, box.min.z, + box.min.x, box.max.y, box.min.z, + box.max.x, box.max.y, box.min.z + ] + end + + def draw_bounding_boxes + @bounding_boxes.each do |key, bounding_box| + glPushMatrix + + glTranslatef( + bounding_box[:entity].position.x, + bounding_box[:entity].position.y, + bounding_box[:entity].position.z + ) + draw_bounding_box(bounding_box) + @bounding_boxes[key][:objects].each {|o| draw_bounding_box(o)} + + glPopMatrix + end + end + + def draw_bounding_box(bounding_box) + glEnableClientState(GL_VERTEX_ARRAY) + glEnableClientState(GL_COLOR_ARRAY) + glEnableClientState(GL_NORMAL_ARRAY) + + glVertexPointer(3, GL_FLOAT, 0, bounding_box[:vertices]) + glColorPointer(3, GL_FLOAT, 0, bounding_box[:colors]) + glNormalPointer(GL_FLOAT, 0, bounding_box[:normals]) + + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) + glDisable(GL_LIGHTING) + glDrawArrays(GL_TRIANGLES, 0, bounding_box[:vertices_size] / 3) + glEnable(GL_LIGHTING) + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) + + glDisableClientState(GL_VERTEX_ARRAY) + glDisableClientState(GL_COLOR_ARRAY) + glDisableClientState(GL_NORMAL_ARRAY) + end + end +end diff --git a/lib/cyberarm_engine/opengl/renderer/g_buffer.rb b/lib/cyberarm_engine/opengl/renderer/g_buffer.rb new file mode 100644 index 0000000..a9ee892 --- /dev/null +++ b/lib/cyberarm_engine/opengl/renderer/g_buffer.rb @@ -0,0 +1,162 @@ +module CyberarmEngine + class GBuffer + attr_reader :screen_vbo, :vertices, :uvs + def initialize(width:, height:) + @width, @height = width, height + + @framebuffer = nil + @buffers = [:position, :diffuse, :normal, :texcoord] + @textures = {} + @screen_vbo = nil + @ready = false + + @vertices = [ + -1.0, -1.0, 0, + 1.0, -1.0, 0, + -1.0, 1.0, 0, + + -1.0, 1.0, 0, + 1.0, -1.0, 0, + 1.0, 1.0, 0, + ].freeze + + @uvs = [ + 0, 0, + 1, 0, + 0, 1, + + 0, 1, + 1, 0, + 1, 1 + ].freeze + + create_framebuffer + create_screen_vbo + end + + def create_framebuffer + buffer = ' ' * 4 + glGenFramebuffers(1, buffer) + @framebuffer = buffer.unpack('L2').first + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, @framebuffer) + + create_textures + + status = glCheckFramebufferStatus(GL_FRAMEBUFFER) + + if status != GL_FRAMEBUFFER_COMPLETE + message = "" + + case status + when GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT + message = "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT" + when GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT + message = "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT" + when GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER + message = "GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER" + when GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER + message = "GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER" + when GL_FRAMEBUFFER_UNSUPPORTED + message = "GL_FRAMEBUFFER_UNSUPPORTED" + else + message = "Unknown error!" + end + puts "Incomplete framebuffer: #{status}\nError: #{message}" + else + @ready = true + end + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0) + end + + def create_textures + @buffers.size.times do |i| + buffer = ' ' * 4 + glGenTextures(1, buffer) + texture_id = buffer.unpack('L2').first + @textures[@buffers[i]] = texture_id + + glBindTexture(GL_TEXTURE_2D, texture_id) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, @width, @height, 0, GL_RGBA, GL_FLOAT, nil) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, texture_id, 0) + end + + buffer = ' ' * 4 + glGenTextures(1, buffer) + texture_id = buffer.unpack('L2').first + @textures[:depth] = texture_id + + glBindTexture(GL_TEXTURE_2D, texture_id) + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, @width, @height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nil) + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, texture_id, 0) + + draw_buffers = [ GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 ] + glDrawBuffers(draw_buffers.size, draw_buffers.pack("I*")) + end + + def create_screen_vbo + buffer = ' ' * 4 + glGenVertexArrays(1, buffer) + @screen_vbo = buffer.unpack('L2').first + + buffer = " " * 4 + glGenBuffers(1, buffer) + @positions_buffer_id = buffer.unpack('L2').first + + buffer = " " * 4 + glGenBuffers(1, buffer) + @uvs_buffer_id = buffer.unpack('L2').first + + glBindVertexArray(@screen_vbo) + glBindBuffer(GL_ARRAY_BUFFER, @positions_buffer_id) + glBufferData(GL_ARRAY_BUFFER, @vertices.size * Fiddle::SIZEOF_FLOAT, @vertices.pack("f*"), GL_STATIC_DRAW); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nil) + + glBindBuffer(GL_ARRAY_BUFFER, @uvs_buffer_id) + glBufferData(GL_ARRAY_BUFFER, @uvs.size * Fiddle::SIZEOF_FLOAT, @uvs.pack("f*"), GL_STATIC_DRAW); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, nil) + + glEnableVertexAttribArray(0) + glEnableVertexAttribArray(1) + + glBindVertexArray(0) + end + + def bind_for_writing + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, @framebuffer) + end + + def bind_for_reading + glBindFramebuffer(GL_READ_FRAMEBUFFER, @framebuffer) + end + + def set_read_buffer(buffer) + glReadBuffer(GL_COLOR_ATTACHMENT0 + @textures.keys.index(buffer)) + end + + def set_read_buffer_depth + glReadBuffer(GL_DEPTH_ATTACHMENT) + end + + def unbind_framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, 0) + end + + def texture(type) + @textures[type] + end + + def clean_up + glDeleteFramebuffers(1, [@framebuffer].pack("L")) + + glDeleteTextures(@textures.values.size, @textures.values.pack("L*")) + + glDeleteBuffers(2, [@positions_buffer_id, @uvs_buffer_id].pack("L*")) + glDeleteVertexArrays(1, [@screen_vbo].pack("L")) + gl_error? + end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/opengl/renderer/opengl_renderer.rb b/lib/cyberarm_engine/opengl/renderer/opengl_renderer.rb new file mode 100644 index 0000000..7d6b06c --- /dev/null +++ b/lib/cyberarm_engine/opengl/renderer/opengl_renderer.rb @@ -0,0 +1,285 @@ +module CyberarmEngine + class OpenGLRenderer + @@immediate_mode_warning = false + + attr_accessor :show_wireframe + def initialize(width:, height:, show_wireframe: false) + @width, @height = width, height + @show_wireframe = show_wireframe + + @g_buffer = GBuffer.new(width: @width, height: @height) + end + + def canvas_size_changed + @g_buffer.unbind_framebuffer + @g_buffer.clean_up + + @g_buffer = GBuffer.new(width: @width, height: @height) + end + + def render(camera, lights, entities) + glViewport(0, 0, @width, @height) + glEnable(GL_DEPTH_TEST) + + if Shader.available?("g_buffer") && Shader.available?("lighting") + @g_buffer.bind_for_writing + gl_error? + + glClearColor(0.0, 0.0, 0.0, 0.0) + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + + Shader.use("g_buffer") do |shader| + gl_error? + + entities.each do |entity| + next unless entity.visible && entity.renderable + + shader.uniform_transform("projection", camera.projection_matrix) + shader.uniform_transform("view", camera.view_matrix) + shader.uniform_transform("model", entity.model_matrix) + shader.uniform_vec3("cameraPosition", camera.position) + + gl_error? + draw_model(entity.model, shader) + entity.draw + end + end + + @g_buffer.unbind_framebuffer + gl_error? + + @g_buffer.bind_for_reading + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0) + + lighting(lights) + gl_error? + + post_processing + gl_error? + + # render_framebuffer + gl_error? + + @g_buffer.unbind_framebuffer + gl_error? + else + puts "Shaders are disabled or failed to compile, using immediate mode for rendering..." unless @@immediate_mode_warning + @@immediate_mode_warning = true + + gl_error? + lights.each(&:draw) + camera.draw + + glEnable(GL_NORMALIZE) + entities.each do |entity| + next unless entity.visible && entity.renderable + + glPushMatrix + + glTranslatef(entity.position.x, entity.position.y, entity.position.z) + glScalef(entity.scale.x, entity.scale.y, entity.scale.z) + glRotatef(entity.orientation.x, 1.0, 0, 0) + glRotatef(entity.orientation.y, 0, 1.0, 0) + glRotatef(entity.orientation.z, 0, 0, 1.0) + + gl_error? + draw_mesh(entity.model) + entity.draw + glPopMatrix + end + end + + gl_error? + end + + def copy_g_buffer_to_screen + @g_buffer.set_read_buffer(:position) + glBlitFramebuffer(0, 0, @g_buffer.width, @g_buffer.height, + 0, 0, @g_buffer.width / 2, @g_buffer.height / 2, + GL_COLOR_BUFFER_BIT, GL_LINEAR) + + @g_buffer.set_read_buffer(:diffuse) + glBlitFramebuffer(0, 0, @g_buffer.width, @g_buffer.height, + 0, @g_buffer.height / 2, @g_buffer.width / 2, @g_buffer.height, + GL_COLOR_BUFFER_BIT, GL_LINEAR) + + @g_buffer.set_read_buffer(:normal) + glBlitFramebuffer(0, 0, @g_buffer.width, @g_buffer.height, + @g_buffer.width / 2, @g_buffer.height / 2, @g_buffer.width, @g_buffer.height, + GL_COLOR_BUFFER_BIT, GL_LINEAR) + + @g_buffer.set_read_buffer(:texcoord) + glBlitFramebuffer(0, 0, @g_buffer.width, @g_buffer.height, + @g_buffer.width / 2, 0, @g_buffer.width, @g_buffer.height / 2, + GL_COLOR_BUFFER_BIT, GL_LINEAR) + end + + def lighting(lights) + Shader.use("lighting") do |shader| + glBindVertexArray(@g_buffer.screen_vbo) + + glDisable(GL_DEPTH_TEST) + glEnable(GL_BLEND) + + glActiveTexture(GL_TEXTURE0) + glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:diffuse)) + shader.uniform_integer("diffuse", 0) + + glActiveTexture(GL_TEXTURE1) + glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:position)) + shader.uniform_integer("position", 1) + + glActiveTexture(GL_TEXTURE2) + glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:texcoord)) + shader.uniform_integer("texcoord", 2) + + glActiveTexture(GL_TEXTURE3) + glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:normal)) + shader.uniform_integer("normal", 3) + + glActiveTexture(GL_TEXTURE4) + glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:depth)) + shader.uniform_integer("depth", 4) + + lights.each_with_index do |light, i| + shader.uniform_integer("light[0].type", light.type); + shader.uniform_vec3("light[0].direction", light.direction) + shader.uniform_vec3("light[0].position", light.position) + shader.uniform_vec3("light[0].diffuse", light.diffuse) + shader.uniform_vec3("light[0].ambient", light.ambient) + shader.uniform_vec3("light[0].specular", light.specular) + + glDrawArrays(GL_TRIANGLES, 0, @g_buffer.vertices.size) + end + + glBindVertexArray(0) + end + end + + def post_processing + end + + def render_framebuffer + if Shader.available?("lighting") + Shader.use("lighting") do |shader| + glBindVertexArray(@g_buffer.screen_vbo) + + glDisable(GL_DEPTH_TEST) + glEnable(GL_BLEND) + + glActiveTexture(GL_TEXTURE0) + glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:diffuse)) + shader.uniform_integer("diffuse_texture", 0) + + glDrawArrays(GL_TRIANGLES, 0, @g_buffer.vertices.size) + + glBindVertexArray(0) + end + end + end + + def draw_model(model, shader) + glBindVertexArray(model.vertex_array_id) + glEnableVertexAttribArray(0) + glEnableVertexAttribArray(1) + glEnableVertexAttribArray(2) + if model.has_texture? + glEnableVertexAttribArray(3) + glEnableVertexAttribArray(4) + end + + if @show_wireframe + glLineWidth(2) + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) + Shader.active_shader.uniform_boolean("disableLighting", true) + + glDrawArrays(GL_TRIANGLES, 0, model.faces.count * 3) + + Shader.active_shader.uniform_boolean("disableLighting", false) + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) + glLineWidth(1) + end + + offset = 0 + model.objects.each do |object| + shader.uniform_boolean("hasTexture", object.has_texture?) + + if object.has_texture? + glBindTexture(GL_TEXTURE_2D, object.materials.find { |mat| mat.texture_id }.texture_id) + else + glBindTexture(GL_TEXTURE_2D, 0) + end + + glDrawArrays(GL_TRIANGLES, offset, object.faces.count * 3) + offset += object.faces.count * 3 + end + + if model.has_texture? + glDisableVertexAttribArray(4) + glDisableVertexAttribArray(3) + + glBindTexture(GL_TEXTURE_2D, 0) + end + glDisableVertexAttribArray(2) + glDisableVertexAttribArray(1) + glDisableVertexAttribArray(0) + + glBindBuffer(GL_ARRAY_BUFFER, 0) + glBindVertexArray(0) + end + + def draw_mesh(model) + model.objects.each_with_index do |o, i| + glEnable(GL_COLOR_MATERIAL) + glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE) + glShadeModel(GL_FLAT) unless o.faces.first[4] + glShadeModel(GL_SMOOTH) if o.faces.first[4] + glEnableClientState(GL_VERTEX_ARRAY) + glEnableClientState(GL_COLOR_ARRAY) + glEnableClientState(GL_NORMAL_ARRAY) + + if o.has_texture? + glEnable(GL_TEXTURE_2D) + glBindTexture(GL_TEXTURE_2D, o.materials.find { |mat| mat.texture_id }.texture_id) + glEnableClientState(GL_TEXTURE_COORD_ARRAY) + glTexCoordPointer(3, GL_FLOAT, 0, o.flattened_uvs) + end + + glVertexPointer(4, GL_FLOAT, 0, o.flattened_vertices) + glColorPointer(3, GL_FLOAT, 0, o.flattened_materials) + glNormalPointer(GL_FLOAT, 0, o.flattened_normals) + + if @show_wireframe # This is kinda expensive + glDisable(GL_LIGHTING) + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) + glPolygonOffset(2, 0.5) + glLineWidth(3) + + glDrawArrays(GL_TRIANGLES, 0, o.flattened_vertices_size/4) + + glLineWidth(1) + glPolygonOffset(0, 0) + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) + glEnable(GL_LIGHTING) + + glDrawArrays(GL_TRIANGLES, 0, o.flattened_vertices_size/4) + else + glDrawArrays(GL_TRIANGLES, 0, o.flattened_vertices_size/4) + end + + # glBindBuffer(GL_ARRAY_BUFFER, 0) + + glDisableClientState(GL_VERTEX_ARRAY) + glDisableClientState(GL_COLOR_ARRAY) + glDisableClientState(GL_NORMAL_ARRAY) + + if o.has_texture? + glDisableClientState(GL_TEXTURE_COORD_ARRAY) + glDisable(GL_TEXTURE_2D) + end + + glDisable(GL_COLOR_MATERIAL) + end + end + end +end diff --git a/lib/cyberarm_engine/opengl/renderer/renderer.rb b/lib/cyberarm_engine/opengl/renderer/renderer.rb new file mode 100644 index 0000000..26c8ad9 --- /dev/null +++ b/lib/cyberarm_engine/opengl/renderer/renderer.rb @@ -0,0 +1,22 @@ +module CyberarmEngine + class Renderer + attr_reader :opengl_renderer, :bounding_box_renderer + + def initialize + @bounding_box_renderer = BoundingBoxRenderer.new + @opengl_renderer = OpenGLRenderer.new(width: $window.width, height: $window.height) + end + + def draw(camera, lights, entities) + @opengl_renderer.render(camera, lights, entities) + @bounding_box_renderer.render(entities) if @show_bounding_boxes + end + + def canvas_size_changed + @opengl_renderer.canvas_size_changed + end + + def finalize # cleanup + end + end +end diff --git a/lib/cyberarm_engine/shader.rb b/lib/cyberarm_engine/opengl/shader.rb similarity index 100% rename from lib/cyberarm_engine/shader.rb rename to lib/cyberarm_engine/opengl/shader.rb diff --git a/lib/cyberarm_engine/opengl/texture.rb b/lib/cyberarm_engine/opengl/texture.rb new file mode 100644 index 0000000..8df9836 --- /dev/null +++ b/lib/cyberarm_engine/opengl/texture.rb @@ -0,0 +1,67 @@ +module CyberarmEngine + class Texture + DEFAULT_TEXTURE = "#{CYBERARM_ENGINE_ROOT_PATH}/assets/textures/default.png" + + CACHE = {} + + def self.release_textures + CACHE.values.each do |id| + glDeleteTextures(id) + end + end + + def self.from_cache(path, retro) + return CACHE.dig("#{path}?retro=#{retro}") + end + + attr_reader :id + def initialize(path: nil, image: nil, retro: false) + raise "keyword :path or :image must be provided!" if path.nil? && image.nil? + @retro = retro + @path = path + + if @path + unless File.exist?(@path) + warn "Missing texture at: #{@path}" + @retro = true # override retro setting + @path = DEFAULT_TEXTURE + end + + if texture = Texture.from_cache(@path, @retro) + @id = texture.id + return + end + + image = load_image(@path) + @id = create_from_image(image) + else + @id = create_from_image(image) + end + end + + def load_image(path) + CACHE["#{path}?retro=#{@retro}"] = self + Gosu::Image.new(path, retro: @retro) + end + + def create_from_image(image) + array_of_pixels = image.to_blob + + tex_names_buf = ' ' * 4 + glGenTextures(1, tex_names_buf) + texture_id = tex_names_buf.unpack('L2').first + + glBindTexture(GL_TEXTURE_2D, texture_id) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, array_of_pixels) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) if @retro + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) unless @retro + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) + glGenerateMipmap(GL_TEXTURE_2D) + gl_error? + + return texture_id + end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/transform.rb b/lib/cyberarm_engine/transform.rb index 1a6c57f..f5878b6 100644 --- a/lib/cyberarm_engine/transform.rb +++ b/lib/cyberarm_engine/transform.rb @@ -196,7 +196,7 @@ module CyberarmEngine ) end - def self.orthographic(left, right, top, bottom, near, far) + def self.orthographic(left, right, bottom, top, near, far) s = Vector.new( 2 / (right - left.to_f), 2 / (top - bottom.to_f), diff --git a/lib/cyberarm_engine/window.rb b/lib/cyberarm_engine/window.rb index d1be708..3bc8011 100644 --- a/lib/cyberarm_engine/window.rb +++ b/lib/cyberarm_engine/window.rb @@ -5,6 +5,7 @@ module CyberarmEngine SONGS = {} attr_accessor :show_cursor + attr_writer :exit_on_opengl_error attr_reader :last_frame_time def self.now @@ -25,6 +26,7 @@ module CyberarmEngine self.caption = "CyberarmEngine #{CyberarmEngine::VERSION} #{Gosu.language}" @states = [] + @exit_on_opengl_error = false setup if defined?(setup) end @@ -49,6 +51,14 @@ module CyberarmEngine @last_frame_time/1000.0 end + def aspect_ratio + width / height.to_f + end + + def exit_on_opengl_error? + @exit_on_opengl_error + end + def button_down(id) super current_state.button_down(id) if current_state