# The contents of this file are subject to the Common Public Attribution License # Version 1.0 (the “License”); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # https://myou.dev/licenses/LICENSE-CPAL. The License is based on the Mozilla # Public License Version 1.1 but Sections 14 and 15 have been added to cover use # of software over a computer network and provide for limited attribution for # the Original Developer. In addition, Exhibit A has been modified to be # consistent with Exhibit B. # # Software distributed under the License is distributed on an “AS IS” basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. # # The Original Code is Myou Engine. # # the Original Developer is the Initial Developer. # # The Initial Developer of the Original Code is the Myou Engine developers. # All portions of the code written by the Myou Engine developers are Copyright # (c) 2024. All Rights Reserved. # # Alternatively, the contents of this file may be used under the terms of the # GNU Affero General Public License version 3 (the [AGPL-3] License), in which # case the provisions of [AGPL-3] License are applicable instead of those above. # # If you wish to allow use of your version of this file only under the terms of # the [AGPL-3] License and not to allow others to use your version of this file # under the CPAL, indicate your decision by deleting the provisions above and # replace them with the notice and other provisions required by the [AGPL-3] # License. If you do not delete the provisions above, a recipient may use your # version of this file under either the CPAL or the [AGPL-3] License. import ../types import std/tables from vmath import Vec4, `$` import ../platform/gl when defined(nimdoc): type TYPES* = VaryingType | Varying | EsPrecision | Material | Shader # Forward declarations func add_line_numbers(s: string, first: int=1):string proc newMaterial*(engine: MyouEngine, name: string, scene: Scene, vertex = "", fragment = "", textures = initOrderedTable[string, Texture](), ubos: seq[UBO] = @[], varyings: seq[Varying] = @[], double_sided = false, glsl_version: string = engine.glsl_version, es_precision = EsPrecisionHigh, defines = initTable[string, string](), ): Material proc newSolidMaterial*(engine: MyouEngine, name: string, color: Vec4): Material proc get_shader*(self: Material, mesh: Mesh): Shader proc delete_all_shaders*(self: Material, destroy: bool = true) proc destroy*(self: Material) proc initShader*(self: Shader, engine: MyouEngine, material: Material, layout: AttributeList, modifiers: seq[VertexModifier], defines: Table[string, string]) proc use*(self: Shader): GLuint {.inline,discardable.} proc destroy*(self: Shader) # End forward declarations import tinyre import std/sequtils import std/strformat import std/strutils import std/options import ../attributes import ../scene import ./render import ./ubo # TODO: move this, detect needed functions, make pluggable, etc. const VERTEX_SHADER_LIBRARY = dedent """ // float linear_to_srgb(float v) { // if(v <= 0.0031308) // return 12.92 * v; // else // return (1.055) * pow(v, 1.0/2.4) - 0.055; // } float srgb_to_linear(float v) { if (v <= 0.04045) return v / 12.92; else return pow((v + 0.055) / (1.055), 2.4); } vec4 srgb_to_linear(vec4 c){ return vec4( srgb_to_linear(c.r), srgb_to_linear(c.g), srgb_to_linear(c.b), c.a ); } """ proc console_error(msg: string) = echo msg func add_line_numbers(s: string, first: int=1):string = var lines = s.strip(false, true, {'\n'}).split("\n") for i,line in lines: lines[i] = $(i+first) & " " & line return lines.join("\n") proc newMaterial*(engine: MyouEngine, name: string, scene: Scene, vertex = "", fragment = "", textures = initOrderedTable[string, Texture](), ubos: seq[UBO] = @[], varyings: seq[Varying] = @[], double_sided = false, glsl_version: string = engine.glsl_version, es_precision = EsPrecisionHigh, defines = initTable[string, string](), ): Material = var self = new Material self.engine = engine self.name = name self.scene = scene self.vertex = vertex self.fragment = fragment self.textures = textures self.ubos = ubos self.varyings = varyings self.double_sided = double_sided self.glsl_version = glsl_version self.es_precision = es_precision self.defines = defines if scene != nil: scene.materials[self.name] = self self.render_scene = self.scene self.shader_library = "" self.use_debug_shaders = false self.last_shader = nil # TODO: use generator here! return self proc newSolidMaterial*(engine: MyouEngine, name: string, color: Vec4): Material = newMaterial(engine, name, nil, fragment=dedent &""" out vec4 glOutColor; void main(){{ glOutColor = {color}; }}""", ) proc newVertexColordMaterial*(engine: MyouEngine, name: string): Material = newMaterial(engine, name, nil, fragment = dedent """ in vec4 vcol; out vec4 glOutColor; void main(){ glOutColor = vcol; }""", varyings = @[Varying(vtype: VertexColor, varname: "vcol")], ) proc get_shader*(self: Material, mesh: Mesh): Shader = # TODO: include vertex modifiers and defines in signature # TODO: generate a signature/hash for each layout and defines table every time # they're modified and not every time we're goint to check them here var shader = self.shaders.get_or_default(mesh.layout) if shader == nil: # self.get_texture_list() # TODO: split shader in two: code generation and compilation # and the second part will be deprecated # so just code gen, we will know all locations beforehand shader = new Shader self.shaders[mesh.layout] = shader self.last_shader = shader shader.initShader(self.engine, self, mesh.data.layout & mesh.data.tf_layout, mesh.vertex_modifiers, mesh.material_defines) return shader proc delete_all_shaders*(self: Material, destroy: bool = true) = if destroy: for shader in values(self.shaders): shader.destroy() self.shaders.clear() self.last_shader = nil proc destroy*(self: Material) = self.delete_all_shaders() self.textures.clear() var id = 0 proc initShader*(self: Shader, engine: MyouEngine, material: Material, layout: AttributeList, modifiers: seq[VertexModifier], defines: Table[string, string]) = self.engine = engine self.material = material id += 1 self.id = id let glsl_version = material.glsl_version let is_glsl_es = glsl_version.endswith "es" let precision = case material.es_precision: of EsPrecisionNone: "" of EsPrecisionLow: "lowp" of EsPrecisionMedium: "mediump" of EsPrecisionHigh: "highp" let precision_block = if precision != "": @[ &"precision {precision} float;", &"precision {precision} int;", &"precision {precision} sampler2DArrayShadow;", ] else: @[] let version_line = if glsl_version.len != 0: @["#version " & glsl_version] else: @[] var fragment_lines = material.fragment.split("\n") # fragment shaders are optional in desktop OpenGL let has_fragment_shader = material.fragment != "" or is_glsl_es if has_fragment_shader: fragment_lines.insert(version_line, 0) var def_index = 0 for i,line in fragment_lines: def_index = i if line.len != 0 and line[0] != '#': break fragment_lines.insert(precision_block, def_index) for k,v in pairs(defines): fragment_lines.insert(&"#define {k} {v}", def_index) for k,v in pairs(material.defines): if k notin defines: fragment_lines.insert(&"#define {k} {v}", def_index) fragment_lines.insert(material.scene.get_lighting_code_defines.join("\n"), def_index) if material.fragment == "": fragment_lines.add "void main(){}" var modifier_ubos: seq[UBO] var vs: string if self.material.vertex != "": vs = self.material.vertex if not vs.startswith "#version": vs = "#version " & glsl_version & "\n" & vs else: let vs_head = @[ precision_block.join("\n"), get_render_uniform_blocks(), ] var attribute_names, attribute_lines: seq[string] for attr in layout: attribute_names.add attr.name attribute_lines.add &"in vec{attr.count} {attr.name};" if "vnormal" notin attribute_names: attribute_lines.add("const vec4 vnormal = vec4(0.0);") var modifiers_uniforms, modifiers_bodies, modifiers_post_bodies: seq[string] var required_extensions: Table[string, bool] for m in modifiers: let code_lines = m.get_code(self.material.varyings) modifiers_uniforms &= code_lines.uniform_lines modifiers_bodies &= code_lines.body_lines modifiers_post_bodies &= code_lines.post_transform_lines for e in code_lines.extensions: required_extensions[e] = true if m.ubo != nil: modifier_ubos.add m.ubo var extension_lines: seq[string] for e in keys(required_extensions): extension_lines.add(&"#extension {e} : require") var varyings_decl: seq[string] var varyings_assign: seq[string] var varyings_uniform_decl: seq[string] var first_uv = true var vertex_shader_library: seq[string] for v in material.varyings: case v.vtype: of Unused: if v.gltype == "int": varyings_decl.add(&"flat out int {v.varname};") varyings_assign.add(&"{v.varname} = 0;") else: varyings_decl.add(&"out {v.gltype} {v.varname}; //unused") let val = v.gltype & "(0.0)" varyings_assign.add(&"{v.varname} = {val};") of ViewPosition: varyings_decl.add(&"out vec3 {v.varname};") varyings_assign.add(&"{v.varname} = view_co.xyz;") of WorldPosition: varyings_decl.add(&"out vec3 {v.varname};") varyings_assign.add(&"{v.varname} = (model_matrix * co).xyz;") of ProjPosition: varyings_decl.add(&"out vec4 {v.varname};") varyings_assign.add(&"{v.varname} = proj_co;") of ViewNormal: varyings_decl.add(&"out vec3 {v.varname};") varyings_assign.add(&"{v.varname} = normalize(normal_matrix * normal);") of WorldNormal: varyings_decl.add(&"out vec3 {v.varname};") varyings_assign.add(&"{v.varname} = normalize((view_matrix_inverse * vec4(normal_matrix * normal, 0.0)).xyz);") of ObjectNormal: # TODO: Can we avoid normalizing? varyings_decl.add(&"out vec3 {v.varname};") varyings_assign.add(&"{v.varname} = normalize(vnormal.xyz);") of Uv: # UV layer var uv_name = v.attname.replace(re"[^_A-Za-z0-9]", "") # TODO: use "uv_active" or something, and yield vec2(0,0) when # uv is not found if uv_name notin attribute_names: # When the UV doesn't exist or is empty, we just use # the first layer in the list or a null vector # TODO: read the active UV layer for render uv_name = "vec2(0.0)" for aname in attribute_names: if not aname.starts_with("uv_"): continue uv_name = aname break let gltype = "vec2" varyings_decl.add(&"out {gltype} {v.varname};") varyings_assign.add(&"{v.varname} = {uv_name};") first_uv = false of VertexColor: # TODO: do we need to add "vc_"? var vc_name = "vc_" & v.attname.replace(re"[^_A-Za-z0-9]", "") if vc_name notin attribute_names: vc_name = "vec4(0.0)" for aname in attribute_names: if not aname.starts_with("vc_"): continue vc_name = aname break var multiplier = "" var color_transform = "" if v.multiplier != 0 and v.multiplier != 1: multiplier = &"*{v.multiplier:0.6f}" elif vc_name in layout and layout[vc_name].dtype == UByte: multiplier = &"*{1/255}" color_transform = "srgb_to_linear" vertex_shader_library = @[VERTEX_SHADER_LIBRARY] varyings_decl.add(&"out vec4 {v.varname};") varyings_assign.add(&"{v.varname} = {color_transform}({vc_name}{multiplier});") of Tangent: # tangent vectors var ta_name = v.attname.replace(re"[^_A-Za-z0-9]", "") var has_tangent = true if ta_name notin attribute_names: # When the tangent layer doesn't exist or is empty, we just use # the first layer in the list or a null vector ta_name = "vec4(1.0, 0.0, 0.0, 1.0)" has_tangent = false for aname in attribute_names: if not aname.starts_with("tg_"): continue ta_name = aname has_tangent = true break varyings_decl.add(&"out vec4 {v.varname};") if has_tangent: varyings_assign.add(&"{v.varname}.xyz = normalize((model_view_matrix * vec4({ta_name}.xyz,0)).xyz);") varyings_assign.add(&"{v.varname}.w = {ta_name}.w;") else: console_error(&"Material {material.name} expects tangents, mesh doesn't have any") varyings_assign.add(&"{v.varname} = vec4(normalize(vnormal.yzx), 1.0);") # varyings_assign.add(&"{v.varname} = vec4(0,0,0,1);") of Orco: modifiers_bodies.insert("vec3 orco = (co.xyz - mesh_center) * mesh_inv_dimensions;") varyings_decl.add(&"out vec3 {v.varname};") varyings_assign.add(&"{v.varname} = orco.xyz;") let vs_body = (@[ "void main(){", "vec4 co = vec4(vertex, 1.0);", "vec3 normal = vnormal.xyz;", ] & modifiers_bodies & (if self.material.fixed_z.is_some and layout.len == 1: # this is only used for background/foreground planes # TODO: Do we need code to handle any point of view? let fixed_z = self.material.fixed_z.get @[ &"vec4 proj_co = vec4(co.xy, {fixed_z}, 1.0);", "vec4 view_co = inverse(projection_matrix) * proj_co;" ] else: @[ &"vec4 view_co = model_view_matrix * co;", &"vec4 proj_co = projection_matrix * view_co;" ]) & (if self.material.point_size.is_some: @[&"gl_PointSize = {self.material.point_size.get};"] else: @[]) & varyings_assign & modifiers_post_bodies & "gl_Position = proj_co;\n}" ).join("\n ") vs = (version_line & extension_lines & vs_head & varyings_uniform_decl & attribute_lines & vertex_shader_library & modifiers_uniforms & varyings_decl & vs_body).join("\n") when not defined(release): self.vs_code = vs let vertex_shader = glCreateShader(GL_VERTEX_SHADER) defer: glDeleteShader(vertex_shader) # TODO: make a pointer array from the unjoined strings, interleaved with "\n" # instead of concatenating and converting var vs2 = vs.cstring glShaderSource(vertex_shader, 1, cast[cstringArray](addr vs2), nil) glCompileShader(vertex_shader) var fragment_shader: GLuint = 0 defer: glDeleteShader(fragment_shader) template fragment:string = fragment_lines.join("\n") if has_fragment_shader: fragment_shader = glCreateShader(GL_FRAGMENT_SHADER) when not defined(release): self.fs_code = fragment var fragment2 = fragment.cstring glShaderSource(fragment_shader, 1, cast[cstringArray](addr fragment2), nil) glCompileShader(fragment_shader) let prog = glCreateProgram() defer: if prog != self.program.GLuint: echo "deleting program" glDeleteProgram(prog) glAttachShader(prog, vertex_shader) if fragment_shader != 0: glAttachShader(prog, fragment_shader) for attr in layout: glBindAttribLocation(prog, attr.location.GLuint, attr.name.cstring) if material.feedback_varyings.len != 0: let fvars = material.feedback_varyings.mapit it.cstring glTransformFeedbackVaryings(prog, fvars.len.GLsizei, cast[cstringArray](addr fvars[0]), GL_INTERLEAVED_ATTRIBS); when defined(myouUseRenderdoc): let name = &"{self.material.name} {self.material.shaders.len}" glObjectLabel(GL_PROGRAM, prog, GLsizei(name.len), name.cstring) glLinkProgram(prog) var success: GLint glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, addr success) if success == 0: echo vs.add_line_numbers(1) let error_msg = dedent &"""Error compiling vertex shader of material {material.name} {get_shader_info_log(vertex_shader)}""" console_error(error_msg) return if fragment_shader != 0: glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, addr success) if success == 0: let gl_log = get_shader_info_log(fragment_shader) let error_msg = &"Error compiling fragment shader of material {material.name}\n{gl_log}" echo error_msg # console_error fragment let lines = fragment.split("\n") if "ERROR: 0:" in gl_log: # Show engine for first error let line = try: error_msg.split(":")[2].parseInt except: 0 # TODO: show only 4 lines of engine but also the previous # line without indent to know which function it is for i in max(1, line - 1000) ..< min(line + 4, lines.len): console_error(&"{i} {lines[i - 1]}") elif gl_log.startsWith "0(": let line = try: error_msg.split({'(',')'})[1].parseInt except: 0 for i in max(1, line - 1000) ..< min(line + 4, lines.len): console_error(&"{i} {lines[i - 1]}") else: for i,line in lines: console_error(&"{i+1} {line}") console_error(error_msg) return glGetProgramiv(prog, GL_LINK_STATUS, addr success) if success == 0: let error_msg = dedent &"""Error linking shader of material {material.name} {material.varyings} {get_program_info_log(prog)}""" console_error("VS =============") console_error(vs) # console_error 'FS =============' # console_error fragment console_error("================") console_error error_msg return self.camera_render_ubo_index = glGetUniformBlockIndex(prog, "CameraRenderUniform") self.object_render_ubo_index = glGetUniformBlockIndex(prog, "ObjectRenderUniform") self.object_ubo_index = glGetUniformBlockIndex(prog, "ObjectUniform") glUseProgram(prog) # TODO: use KHR_parallel_shader_compile var ubo_names = newSeqOfCap[string](material.ubos.len) self.ubos.setLen 0 for ubo in material.ubos & modifier_ubos: let idx = ubo.get_index prog if idx == GL_INVALID_INDEX: if ubo.byte_storage.len != 0: echo &"Error: Index of UBO {ubo.name} could not be found in {material.name}" continue self.ubos.add ubo self.ubo_indices.add idx assert ubo.name notin ubo_names, "There's more than one UBO with name " & ubo.name & " in {material.name}" ubo_names.add ubo.name ubo_names.setLen 0 self.texture_locations.setLen 0 var extra_location_count = 0 for name in material.textures.keys: self.texture_locations.add glGetUniformLocation(prog, name.cstring) if defined(myouEnsureTextureLocations) and not defined(release): assert self.texture_locations[^1] != -1, "invalid texture location for " & name self.cubemap_locations.setLen 0 self.shadowmap_location = -1 if material.scene != nil: self.shadowmap_location = glGetUniformLocation(prog, "shadow_maps".cstring) if self.shadowmap_location != -1: extra_location_count.inc for i in 0 ..< material.scene.max_cubemaps: # TODO: test with a single glUniform1iv instead of changing # the individual array elements self.cubemap_locations.add glGetUniformLocation(prog, (&"cube_maps[{i}]").cstring) # when not defined(release): # assert self.shadowmap_locations[^1] != -1 # dump material.name # dump self.cubemap_locations assert self.texture_locations.len + self.cubemap_locations.len + extra_location_count <= self.engine.renderer.max_textures self.program = prog.GPUProgram proc use*(self: Shader): GLuint {.inline,discardable.} = glUseProgram(self.program.GLuint) return self.program.GLuint proc destroy*(self: Shader) = self.program = 0.GPUProgram