534 lines
22 KiB
Nim
534 lines
22 KiB
Nim
# 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
|
|
|
|
|