|
@@ -0,0 +1,583 @@
|
|
|
+"""Class represents fragment shader and vertex shader"""
|
|
|
+import re
|
|
|
+import collections
|
|
|
+import bpy
|
|
|
+import mathutils
|
|
|
+from .shader_functions import find_function_by_name
|
|
|
+from ...structures import Array, ValidationError
|
|
|
+
|
|
|
+
|
|
|
+class Variable:
|
|
|
+ """A variable in material shader scripts"""
|
|
|
+
|
|
|
+ def __init__(self, var_type, var_name):
|
|
|
+ self.type = var_type
|
|
|
+ self.name = var_name
|
|
|
+
|
|
|
+ def __str__(self):
|
|
|
+ """Convert to string"""
|
|
|
+ return self.name
|
|
|
+
|
|
|
+
|
|
|
+class Value:
|
|
|
+ """A constant value in material shader scripts"""
|
|
|
+
|
|
|
+ def __init__(self, type_str, data):
|
|
|
+ self.type = type_str
|
|
|
+ self.data = data
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def create_from_blender_value(cls, blender_value):
|
|
|
+ """Creaate a Value() from a blender object"""
|
|
|
+ if isinstance(
|
|
|
+ blender_value, (bpy.types.bpy_prop_array, mathutils.Vector)):
|
|
|
+ tmp = list()
|
|
|
+ for val in blender_value:
|
|
|
+ tmp.append(val)
|
|
|
+
|
|
|
+ return Value("vec{}".format(len(tmp)), tuple(tmp))
|
|
|
+
|
|
|
+ if isinstance(blender_value, mathutils.Matrix):
|
|
|
+ # godot mat is column major order
|
|
|
+ mat = blender_value.transposed()
|
|
|
+ column_vec_list = list()
|
|
|
+ for vec in mat:
|
|
|
+ column_vec_list.append(cls.create_from_blender_value(vec))
|
|
|
+
|
|
|
+ return Value(
|
|
|
+ "mat{}".format(len(column_vec_list)),
|
|
|
+ tuple(column_vec_list)
|
|
|
+ )
|
|
|
+
|
|
|
+ return Value("float", blender_value)
|
|
|
+
|
|
|
+ def __str__(self):
|
|
|
+ """Convert to string"""
|
|
|
+ if self.type.startswith(('vec', 'mat')):
|
|
|
+ return "{}({})".format(
|
|
|
+ self.type,
|
|
|
+ ', '.join([str(x) for x in self.data])
|
|
|
+ )
|
|
|
+ return str(self.data)
|
|
|
+
|
|
|
+
|
|
|
+class FragmentBSDFContainer:
|
|
|
+ """Several attributes altogether represents
|
|
|
+ blender shader output closure"""
|
|
|
+ _ATTRIBUTES_META = collections.OrderedDict([
|
|
|
+ ('albedo', 'vec3'),
|
|
|
+ ('alpha', 'float'),
|
|
|
+ ('sss_strength', 'float'),
|
|
|
+ ('specular', 'float'),
|
|
|
+ ('metallic', 'float'),
|
|
|
+ ('roughness', 'float'),
|
|
|
+ ('clearcoat', 'float'),
|
|
|
+ ('clearcoat_gloss', 'float'),
|
|
|
+ ('anisotropy', 'float'),
|
|
|
+ ('transmission', 'float'),
|
|
|
+ ('ior', 'float'),
|
|
|
+ ('emission', 'vec3'),
|
|
|
+ ('normal', 'vec3'),
|
|
|
+ ('tangent', 'vec3'),
|
|
|
+ ])
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self._data = collections.OrderedDict()
|
|
|
+
|
|
|
+ def get_attribute(self, attr_name):
|
|
|
+ """Get a property value, if the property is empty return None"""
|
|
|
+ return self._data.get(attr_name, None)
|
|
|
+
|
|
|
+ def set_attribute(self, attr_name, attr_value):
|
|
|
+ """Set a property, note that property value can be
|
|
|
+ either Value() or Variable()"""
|
|
|
+ self._data[attr_name] = attr_value
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def attribute_names_iterable(cls):
|
|
|
+ """Return an iteralble of all attribute names"""
|
|
|
+ return cls._ATTRIBUTES_META.keys()
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def attribute_type(cls, attr_name):
|
|
|
+ """Return a type a given attribute"""
|
|
|
+ return cls._ATTRIBUTES_META[attr_name]
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def default(cls):
|
|
|
+ """Default closure for unconnected socket"""
|
|
|
+ new_closure = cls()
|
|
|
+ new_closure.set_attribute('albedo', Value('vec3', (0.0, 0.0, 0.0)))
|
|
|
+ return new_closure
|
|
|
+
|
|
|
+
|
|
|
+class BaseShader:
|
|
|
+ """Shared methods in vertex shader and fragment shader"""
|
|
|
+ def __init__(self, formated_array, global_ref):
|
|
|
+ # array of code
|
|
|
+ self.code_array = formated_array
|
|
|
+
|
|
|
+ # reference of global scripts,
|
|
|
+ # used to create uniform, add function
|
|
|
+ self.global_ref = global_ref
|
|
|
+
|
|
|
+ # maintain a mapping from all output sockets
|
|
|
+ # to already calculated var
|
|
|
+ self._socket_to_var_map = dict()
|
|
|
+ # use to create unique variable name
|
|
|
+ self._variable_count = 0
|
|
|
+
|
|
|
+ def append_code_line(self, code_pattern, variables=()):
|
|
|
+ """Format a line of code string and append it to codes"""
|
|
|
+ assert code_pattern[-1] == ';'
|
|
|
+ self.code_array.append(
|
|
|
+ code_pattern.format(*tuple([str(x) for x in variables]))
|
|
|
+ )
|
|
|
+
|
|
|
+ def _append_defination_code(self, var_to_define):
|
|
|
+ definition_str = '{} {};'.format(
|
|
|
+ var_to_define.type, str(var_to_define)
|
|
|
+ )
|
|
|
+ self.code_array.append(definition_str)
|
|
|
+
|
|
|
+ def define_variable(self, var_type, var_base_name):
|
|
|
+ """Create a unique variable, and define it in the shader script"""
|
|
|
+ self._variable_count += 1
|
|
|
+ raw_var_name = 'var{}_{}'.format(
|
|
|
+ self._variable_count,
|
|
|
+ var_base_name,
|
|
|
+ )
|
|
|
+ var_name = re.sub(r'\W', '', raw_var_name)
|
|
|
+ new_var = Variable(var_type, var_name)
|
|
|
+ self._append_defination_code(new_var)
|
|
|
+ return new_var
|
|
|
+
|
|
|
+ def define_variable_from_socket(self, node, socket):
|
|
|
+ """Create a unique variable, variable name and type generate
|
|
|
+ from socket, and also define it in the shader script"""
|
|
|
+ if socket.type == 'RGBA':
|
|
|
+ var_type = 'vec4'
|
|
|
+ elif socket.type == 'VECTOR':
|
|
|
+ var_type = 'vec3'
|
|
|
+ elif socket.type == 'VALUE':
|
|
|
+ var_type = 'float'
|
|
|
+ else:
|
|
|
+ raise ValidationError(
|
|
|
+ "socket '{}' at '{}' is incorrectly connected".format(
|
|
|
+ socket.identifier, node.name
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ self._variable_count += 1
|
|
|
+ raw_var_name = 'var{}_{}_{}'.format(
|
|
|
+ self._variable_count,
|
|
|
+ node.name,
|
|
|
+ socket.identifier
|
|
|
+ )
|
|
|
+ var_name = re.sub(r'\W', '', raw_var_name)
|
|
|
+ new_var = Variable(var_type, var_name)
|
|
|
+ self._append_defination_code(new_var)
|
|
|
+ return new_var
|
|
|
+
|
|
|
+ def append_assignment_code(self, var_to_write, var_to_read):
|
|
|
+ """Assign a variable or value to another variable"""
|
|
|
+ assignment_str = '{} = {};'.format(
|
|
|
+ str(var_to_write), str(var_to_read)
|
|
|
+ )
|
|
|
+ self.code_array.append(assignment_str)
|
|
|
+
|
|
|
+ def is_socket_cached(self, input_socket):
|
|
|
+ """Return bool indicating whether an input socket
|
|
|
+ has a variable cached"""
|
|
|
+ if input_socket.links[0].from_socket in self._socket_to_var_map:
|
|
|
+ return True
|
|
|
+ return False
|
|
|
+
|
|
|
+ def fetch_variable_from_socket(self, input_socket):
|
|
|
+ """Given a input socket, return the variable assigned to that
|
|
|
+ socket, note that the variable is actually from the output socket
|
|
|
+ at the other side of the link"""
|
|
|
+ socket_link = input_socket.links[0]
|
|
|
+ var_from_link = self._socket_to_var_map[socket_link.from_socket]
|
|
|
+
|
|
|
+ if socket_link.from_socket.type == socket_link.to_socket.type:
|
|
|
+ return var_from_link
|
|
|
+
|
|
|
+ # if the type of two sockets are not matched,
|
|
|
+ # insert an implicit conversion
|
|
|
+ return self._implicit_socket_convert(
|
|
|
+ var_from_link,
|
|
|
+ socket_link.from_socket.type,
|
|
|
+ socket_link.to_socket.type,
|
|
|
+ )
|
|
|
+
|
|
|
+ def assign_variable_to_socket(self, output_socket, variable):
|
|
|
+ """Assign an output socket with a variable for later use"""
|
|
|
+ self._socket_to_var_map[output_socket] = variable
|
|
|
+
|
|
|
+ def _implicit_socket_convert(self, src_variable,
|
|
|
+ from_socket_type, to_socket_type):
|
|
|
+ """Implicitly convert variable type between a pair of socket with
|
|
|
+ different type. It is performed when you link two socket with
|
|
|
+ different color in node editor"""
|
|
|
+ if (to_socket_type == 'VALUE' and
|
|
|
+ from_socket_type in ('VECTOR', 'RGBA')):
|
|
|
+ converted_var = self.define_variable(
|
|
|
+ 'float', 'auto_insert_RGBtoBW'
|
|
|
+ )
|
|
|
+ if from_socket_type == 'VECTOR':
|
|
|
+ src_variable = Value('vec4', (src_variable, 1.0))
|
|
|
+
|
|
|
+ function = find_function_by_name('node_rgb_to_bw')
|
|
|
+ self.add_function_call(function, [src_variable], [converted_var])
|
|
|
+ return converted_var
|
|
|
+
|
|
|
+ if to_socket_type == 'VECTOR' and from_socket_type == 'VALUE':
|
|
|
+ return Value('vec3', (src_variable,) * 3)
|
|
|
+
|
|
|
+ if to_socket_type == 'RGBA' and from_socket_type == 'VALUE':
|
|
|
+ return Value('vec4', (src_variable,) * 4)
|
|
|
+
|
|
|
+ if to_socket_type == 'RGBA' and from_socket_type == 'VECTOR':
|
|
|
+ converted_var = self.define_variable(
|
|
|
+ 'vec4', 'auto_insert_VecToColor'
|
|
|
+ )
|
|
|
+ self.append_code_line(
|
|
|
+ ('{} = vec4(clamp({}, vec3(0.0, 0.0, 0.0),'
|
|
|
+ 'vec3(1.0, 1.0, 1.0)).xyz, 1.0);'),
|
|
|
+ (converted_var, src_variable)
|
|
|
+ )
|
|
|
+ return converted_var
|
|
|
+
|
|
|
+ if to_socket_type == 'VECTOR' and from_socket_type == 'RGBA':
|
|
|
+ converted_var = self.define_variable(
|
|
|
+ 'vec3', 'auto_insert_ColorToVec'
|
|
|
+ )
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = {}.xyz;', (converted_var, src_variable)
|
|
|
+ )
|
|
|
+ return converted_var
|
|
|
+
|
|
|
+ raise ValidationError(
|
|
|
+ "Cannot link two sockets with type '{}' and '{}'".format(
|
|
|
+ from_socket_type, to_socket_type
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _function_call_type_check(argument_var_list, param_type_list):
|
|
|
+ assert len(argument_var_list) == len(param_type_list)
|
|
|
+ for index, var in enumerate(argument_var_list):
|
|
|
+ assert var.type == param_type_list[index]
|
|
|
+
|
|
|
+ def add_function_call(self, function, in_arguments, out_arguments):
|
|
|
+ """Call function in shader scripts"""
|
|
|
+ self.global_ref.add_function(function)
|
|
|
+
|
|
|
+ # runtime check to make sure generated scripts is valid
|
|
|
+ self._function_call_type_check(in_arguments, function.in_param_types)
|
|
|
+ self._function_call_type_check(out_arguments, function.out_param_types)
|
|
|
+
|
|
|
+ invoke_str = '{}({}, {});'.format(
|
|
|
+ function.name,
|
|
|
+ ', '.join([str(x) for x in in_arguments]),
|
|
|
+ ', '.join([str(x) for x in out_arguments])
|
|
|
+ )
|
|
|
+ self.code_array.append(invoke_str)
|
|
|
+
|
|
|
+ def zup_to_yup(self, var_to_convert):
|
|
|
+ """Convert a vec3 from z-up space to y-up space"""
|
|
|
+ assert var_to_convert.type == 'vec3'
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = mat3(vec3(1, 0, 0), vec3(0, 0, -1), vec3(0, 1, 0)) * {};',
|
|
|
+ (var_to_convert, var_to_convert)
|
|
|
+ )
|
|
|
+
|
|
|
+ def yup_to_zup(self, var_to_convert):
|
|
|
+ """Convert a vec3 from y-up space to z-up space"""
|
|
|
+ assert var_to_convert.type == 'vec3'
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = mat3(vec3(1, 0, 0), vec3(0, 0, 1), vec3(0, -1, 0)) * {};',
|
|
|
+ (var_to_convert, var_to_convert)
|
|
|
+ )
|
|
|
+
|
|
|
+ def to_string(self):
|
|
|
+ """Serialze"""
|
|
|
+ return self.code_array.to_string()
|
|
|
+
|
|
|
+
|
|
|
+class FragmentShader(BaseShader):
|
|
|
+ """Fragment shader Script"""
|
|
|
+ def __init__(self, global_ref):
|
|
|
+ super().__init__(
|
|
|
+ Array(
|
|
|
+ prefix='\nvoid fragment() {\n\t',
|
|
|
+ seperator='\n\t',
|
|
|
+ suffix='\n}\n'
|
|
|
+ ),
|
|
|
+ global_ref
|
|
|
+ )
|
|
|
+
|
|
|
+ # flag would be set when glass_bsdf is used
|
|
|
+ self.glass_effect = False
|
|
|
+
|
|
|
+ self._invert_view_mat = None
|
|
|
+ self._invert_model_mat = None
|
|
|
+
|
|
|
+ @property
|
|
|
+ def invert_view_mat(self):
|
|
|
+ """Return inverted view matrix"""
|
|
|
+ if self._invert_view_mat is None:
|
|
|
+ self._invert_view_mat = self.define_variable(
|
|
|
+ 'mat4', 'inverted_view_matrix'
|
|
|
+ )
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = inverse({});',
|
|
|
+ (self._invert_view_mat, Variable('mat4', 'INV_CAMERA_MATRIX'))
|
|
|
+ )
|
|
|
+ return self._invert_view_mat
|
|
|
+
|
|
|
+ @property
|
|
|
+ def invert_model_mat(self):
|
|
|
+ """Return inverted model matrix"""
|
|
|
+ if self._invert_model_mat is None:
|
|
|
+ self._invert_model_mat = self.define_variable(
|
|
|
+ 'mat4', 'inverted_model_matrix'
|
|
|
+ )
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = inverse({});',
|
|
|
+ (self._invert_model_mat, Variable('mat4', 'WORLD_MATRIX'))
|
|
|
+ )
|
|
|
+ return self._invert_model_mat
|
|
|
+
|
|
|
+ def add_bsdf_surface(self, bsdf_output):
|
|
|
+ """Link bsdf output to godot fragment builtin out qualifiers"""
|
|
|
+ for name in ('albedo', 'sss_strength', 'specular', 'metallic',
|
|
|
+ 'roughness', 'clearcoat', 'clearcoat_gloss', 'emission',
|
|
|
+ 'normal'):
|
|
|
+ var = bsdf_output.get_attribute(name)
|
|
|
+ if var is not None:
|
|
|
+ self.code_array.append(
|
|
|
+ '{} = {};'.format(name.upper(), str(var))
|
|
|
+ )
|
|
|
+ # xxx: transmission for thick object is not supported in godot
|
|
|
+ # transmission_var = bsdf_output.get_attribute('transmission')
|
|
|
+ # if transmission_var is not None:
|
|
|
+ # self.append_code_line(
|
|
|
+ # 'TRANSMISSION = vec3(1.0, 1.0, 1.0) * {};'
|
|
|
+ # (transmission_var,)
|
|
|
+ # )
|
|
|
+
|
|
|
+ tangent = bsdf_output.get_attribute('tangent')
|
|
|
+ anisotropy = bsdf_output.get_attribute('anisotropy')
|
|
|
+ if tangent is not None and anisotropy is not None:
|
|
|
+ self.append_code_line('ANISOTROPY = {};', (anisotropy,))
|
|
|
+ self.append_code_line(
|
|
|
+ 'TANGENT = normalize(cross(cross({}, NORMAL), NORMAL));',
|
|
|
+ (tangent,),
|
|
|
+ )
|
|
|
+ self.append_code_line('BINORMAL = cross(TANGENT, NORMAL);')
|
|
|
+
|
|
|
+ alpha = bsdf_output.get_attribute('alpha')
|
|
|
+ if alpha is not None:
|
|
|
+ refraction_offset = self.global_ref.define_uniform(
|
|
|
+ 'float', 'refraction_offset'
|
|
|
+ )
|
|
|
+ if self.glass_effect:
|
|
|
+ fresnel_func = find_function_by_name('refraction_fresnel')
|
|
|
+ in_arguments = list()
|
|
|
+ in_arguments.append(Variable('vec3', 'VERTEX'))
|
|
|
+ in_arguments.append(Variable('vec3', 'NORMAL'))
|
|
|
+ in_arguments.append(bsdf_output.get_attribute('ior'))
|
|
|
+ self.add_function_call(
|
|
|
+ fresnel_func, in_arguments, [alpha]
|
|
|
+ )
|
|
|
+ self.append_code_line(
|
|
|
+ 'EMISSION += textureLod(SCREEN_TEXTURE, SCREEN_UV - '
|
|
|
+ 'NORMAL.xy * {}, ROUGHNESS).rgb * (1.0 - {});',
|
|
|
+ (refraction_offset, alpha)
|
|
|
+ )
|
|
|
+ self.append_code_line(
|
|
|
+ 'ALBEDO *= {};',
|
|
|
+ (alpha,),
|
|
|
+ )
|
|
|
+ self.append_code_line(
|
|
|
+ 'ALPHA = 1.0;'
|
|
|
+ )
|
|
|
+
|
|
|
+ def add_bump_displacement(self, displacement_output):
|
|
|
+ """Add bump displacement to fragment shader"""
|
|
|
+ # xxx: use tangent space if uv exists?
|
|
|
+ function = find_function_by_name('node_bump')
|
|
|
+
|
|
|
+ in_arguments = list()
|
|
|
+ # default bump parameters
|
|
|
+ in_arguments.append(Value('float', 1.0))
|
|
|
+ in_arguments.append(Value('float', 0.1))
|
|
|
+ in_arguments.append(displacement_output)
|
|
|
+ in_arguments.append(Variable('vec3', 'NORMAL'))
|
|
|
+ in_arguments.append(Variable('vec3', 'VERTEX'))
|
|
|
+ in_arguments.append(Value('float', 0.0))
|
|
|
+
|
|
|
+ out = Variable('vec3', 'NORMAL')
|
|
|
+ self.add_function_call(function, in_arguments, [out])
|
|
|
+
|
|
|
+ def view_to_model(self, var_to_convert, is_direction=True):
|
|
|
+ """Convert a vec3 from view space to model space,
|
|
|
+ note that conversion is done in y-up space"""
|
|
|
+ assert var_to_convert.type == 'vec3'
|
|
|
+ if is_direction:
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = normalize({} * ({} * vec4({}, 0.0))).xyz;',
|
|
|
+ (var_to_convert, self.invert_model_mat,
|
|
|
+ self.invert_view_mat, var_to_convert)
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = ({} * ({} * vec4({}, 1.0))).xyz;',
|
|
|
+ (var_to_convert, self.invert_model_mat,
|
|
|
+ self.invert_view_mat, var_to_convert)
|
|
|
+ )
|
|
|
+
|
|
|
+ def model_to_view(self, var_to_convert, is_direction=True):
|
|
|
+ """Convert a vec3 from model space to view space,
|
|
|
+ note that conversion is done in y-up space"""
|
|
|
+ assert var_to_convert.type == 'vec3'
|
|
|
+ view_mat = Variable('mat4', 'INV_CAMERA_MATRIX')
|
|
|
+ model_mat = Variable('mat4', 'WORLD_MATRIX')
|
|
|
+ if is_direction:
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = normalize({} * ({} * vec4({}, 0.0))).xyz;',
|
|
|
+ (var_to_convert, view_mat, model_mat, var_to_convert)
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = ({} * ({} * vec4({}, 1.0))).xyz;',
|
|
|
+ (var_to_convert, view_mat, model_mat, var_to_convert)
|
|
|
+ )
|
|
|
+
|
|
|
+ def view_to_world(self, var_to_convert, is_direction=True):
|
|
|
+ """Convert a vec3 from view space to world space,
|
|
|
+ note that it is done in y-up space"""
|
|
|
+ assert var_to_convert.type == 'vec3'
|
|
|
+ if is_direction:
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = normalize({} * vec4({}, 0.0)).xyz;',
|
|
|
+ (var_to_convert, self.invert_view_mat, var_to_convert)
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = ({} * vec4({}, 1.0)).xyz;',
|
|
|
+ (var_to_convert, self.invert_view_mat, var_to_convert)
|
|
|
+ )
|
|
|
+
|
|
|
+ def world_to_view(self, var_to_convert, is_direction=True):
|
|
|
+ """Convert a vec3 from world space to view space,
|
|
|
+ note that it is done in y-up space"""
|
|
|
+ assert var_to_convert.type == 'vec3'
|
|
|
+ view_mat = Variable('mat4', 'INV_CAMERA_MATRIX')
|
|
|
+ if is_direction:
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = normalize({} * vec4({}, 0.0)).xyz;',
|
|
|
+ (var_to_convert, view_mat, var_to_convert)
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ self.append_code_line(
|
|
|
+ '{} = ({} * vec4({}, 1.0)).xyz;',
|
|
|
+ (var_to_convert, view_mat, var_to_convert)
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+class VertexShader(BaseShader):
|
|
|
+ """Vertex shader scripts"""
|
|
|
+ def __init__(self, global_ref):
|
|
|
+ super().__init__(
|
|
|
+ Array(
|
|
|
+ prefix='\nvoid vertex() {\n\t',
|
|
|
+ seperator='\n\t',
|
|
|
+ suffix='\n}\n'
|
|
|
+ ),
|
|
|
+ global_ref
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+class ShaderGlobals:
|
|
|
+ """Global space of shader material, maintains uniforms, functions
|
|
|
+ and rendering configures."""
|
|
|
+ def __init__(self):
|
|
|
+ # render mode and render type is also
|
|
|
+ # placed here
|
|
|
+ self.uniform_codes = Array(
|
|
|
+ prefix='',
|
|
|
+ seperator='\n',
|
|
|
+ suffix='\n'
|
|
|
+ )
|
|
|
+
|
|
|
+ # cache function names to avoid duplicated
|
|
|
+ # function code being added
|
|
|
+ self.function_name_set = set()
|
|
|
+ self.function_codes = Array(
|
|
|
+ prefix='',
|
|
|
+ seperator='\n',
|
|
|
+ suffix='\n'
|
|
|
+ )
|
|
|
+
|
|
|
+ self.textures = dict()
|
|
|
+
|
|
|
+ self._render_mode = Array(
|
|
|
+ prefix='render_mode ',
|
|
|
+ seperator=',',
|
|
|
+ suffix=';'
|
|
|
+ )
|
|
|
+
|
|
|
+ self._set_render_mode()
|
|
|
+ self._uniform_var_count = 0
|
|
|
+
|
|
|
+ self.fragment_shader = FragmentShader(self)
|
|
|
+ self.vertex_shader = VertexShader(self)
|
|
|
+
|
|
|
+ def _set_render_mode(self):
|
|
|
+ self._render_mode.extend([
|
|
|
+ 'blend_mix',
|
|
|
+ 'depth_draw_always',
|
|
|
+ 'cull_back',
|
|
|
+ 'diffuse_burley',
|
|
|
+ 'specular_schlick_ggx',
|
|
|
+ ])
|
|
|
+
|
|
|
+ def add_function(self, function):
|
|
|
+ """Add function body to global codes"""
|
|
|
+ if function.name not in self.function_name_set:
|
|
|
+ self.function_name_set.add(function.name)
|
|
|
+ self.function_codes.append(function.code)
|
|
|
+
|
|
|
+ def define_uniform(self, uni_type, uni_base_name):
|
|
|
+ """Define an uniform variable"""
|
|
|
+ self._uniform_var_count += 1
|
|
|
+ raw_var_name = 'uni{}_{}'.format(
|
|
|
+ self._uniform_var_count,
|
|
|
+ uni_base_name,
|
|
|
+ )
|
|
|
+ var_name = re.sub(r'\W', '', raw_var_name)
|
|
|
+ new_var = Variable(uni_type, var_name)
|
|
|
+ def_str = 'uniform {} {};'.format(uni_type, var_name)
|
|
|
+ self.uniform_codes.append(def_str)
|
|
|
+ return new_var
|
|
|
+
|
|
|
+ def add_image_texture(self, uniform_var, image):
|
|
|
+ """Define a uniform referring to the texture sampler
|
|
|
+ and store the image object"""
|
|
|
+ # store image
|
|
|
+ if image is not None:
|
|
|
+ self.textures[image] = uniform_var
|
|
|
+
|
|
|
+ def to_string(self):
|
|
|
+ """Serialization"""
|
|
|
+ return '\n'.join([
|
|
|
+ "shader_type spatial;", # shader type is spatial for 3D scene
|
|
|
+ self._render_mode.to_string(),
|
|
|
+ self.uniform_codes.to_string(),
|
|
|
+ self.function_codes.to_string(),
|
|
|
+ self.vertex_shader.to_string(),
|
|
|
+ self.fragment_shader.to_string(),
|
|
|
+ ])
|