material-maker/addons/material_maker/engine/gen_material.gd
2020-02-19 23:24:34 +01:00

341 lines
11 KiB
GDScript

tool
extends MMGenShader
class_name MMGenMaterial
var export_paths = {}
var material : SpatialMaterial
var generated_textures = {}
const TEXTURE_LIST = [
{ port=0, texture="albedo", sources=[0] },
{ port=1, texture="orm", sources=[1, 2, 5] },
{ port=2, texture="emission", sources=[3] },
{ port=3, texture="normal", sources=[4] },
{ port=4, texture="depth", sources=[6] },
{ port=5, texture="sss", sources=[7] }
]
const INPUT_ALBEDO : int = 0
const INPUT_METALLIC : int = 1
const INPUT_ROUGHNESS : int = 2
const INPUT_EMISSION : int = 3
const INPUT_NORMAL : int = 4
const INPUT_OCCLUSION : int = 5
const INPUT_DEPTH : int = 6
const INPUT_SSS : int = 7
# The minimum allowed texture size as a power-of-two exponent
const TEXTURE_SIZE_MIN = 4 # 16x16
# The maximum allowed texture size as a power-of-two exponent
const TEXTURE_SIZE_MAX = 12 # 4096x4096
# The default texture size as a power-of-two exponent
const TEXTURE_SIZE_DEFAULT = 10 # 1024x1024
func _ready() -> void:
for t in TEXTURE_LIST:
generated_textures[t.texture] = null
material = SpatialMaterial.new()
func can_be_deleted() -> bool:
return false
func get_type() -> String:
return "material"
func get_type_name() -> String:
return "Material"
func get_output_defs() -> Array:
return []
func get_image_size() -> int:
var rv : int
if parameters.has("size"):
rv = int(pow(2, parameters.size))
else:
rv = int(pow(2, TEXTURE_SIZE_DEFAULT))
return rv
func update_preview() -> void:
var graph_edit = self
while graph_edit is MMGenBase:
graph_edit = graph_edit.get_parent()
if graph_edit != null and graph_edit.has_method("send_changed_signal"):
graph_edit.send_changed_signal()
func set_parameter(p, v) -> void:
.set_parameter(p, v)
update_preview()
func source_changed(input_index : int) -> void:
for t in TEXTURE_LIST:
if t.has("sources") and t.sources.find(input_index) != -1:
generated_textures[t.texture] = null
update_preview()
func render_textures() -> void:
for t in TEXTURE_LIST:
var texture = null
var result
if t.has("port"):
if generated_textures[t.texture] != null:
continue
result = render(t.port, get_image_size())
else:
generated_textures[t.texture] = null
continue
while result is GDScriptFunctionState:
result = yield(result, "completed")
texture = ImageTexture.new()
result.copy_to_texture(texture)
result.release()
# To work, this must be set after calling `copy_to_texture()`
texture.flags |= ImageTexture.FLAG_ANISOTROPIC_FILTER
# Disable filtering for small textures, as they're considered to be used
# for a pixel art style
if texture.get_size().x <= 128:
texture.flags ^= ImageTexture.FLAG_FILTER
generated_textures[t.texture] = texture
func update_materials(material_list) -> void:
render_textures()
for m in material_list:
update_material(m)
func get_generated_texture(slot, file_prefix = null) -> ImageTexture:
if file_prefix != null:
var file_name = "%s_%s.png" % [ file_prefix, slot ]
if File.new().file_exists(file_name):
var texture = load(file_name)
return texture
else:
return null
else:
return generated_textures[slot]
func update_material(m, file_prefix = null) -> void:
var texture
if m is SpatialMaterial:
# Make the material double-sided for better visiblity in the preview
m.params_cull_mode = SpatialMaterial.CULL_DISABLED
# Albedo
m.albedo_color = parameters.albedo_color
m.albedo_texture = get_generated_texture("albedo", file_prefix)
# Ambient occlusion
if get_source(INPUT_OCCLUSION) != null:
m.ao_enabled = true
m.ao_light_affect = parameters.ao
m.ao_texture = get_generated_texture("orm", file_prefix)
m.ao_texture_channel = SpatialMaterial.TEXTURE_CHANNEL_RED
else:
m.ao_enabled = false
# Roughness
m.roughness = parameters.roughness
if get_source(INPUT_ROUGHNESS) != null:
m.roughness_texture = get_generated_texture("orm", file_prefix)
m.roughness_texture_channel = SpatialMaterial.TEXTURE_CHANNEL_GREEN
else:
m.roughness_texture = null
# Metallic
m.metallic = parameters.metallic
if get_source(INPUT_METALLIC) != null:
m.metallic_texture = get_generated_texture("orm", file_prefix)
m.metallic_texture_channel = SpatialMaterial.TEXTURE_CHANNEL_BLUE
else:
m.metallic_texture = null
# Emission
if get_source(INPUT_EMISSION) != null:
m.emission_enabled = true
m.emission_energy = parameters.emission_energy
m.emission_texture = get_generated_texture("emission", file_prefix)
else:
m.emission_enabled = false
# Normal map
if get_source(INPUT_NORMAL) != null:
m.normal_enabled = true
m.normal_texture = get_generated_texture("normal", file_prefix)
m.normal_scale = parameters.normal
else:
m.normal_enabled = false
# Depth
if get_source(INPUT_DEPTH) != null and parameters.depth_scale > 0:
m.depth_enabled = true
m.depth_deep_parallax = true
m.depth_scale = parameters.depth_scale * 0.2
m.depth_texture = get_generated_texture("depth", file_prefix)
else:
m.depth_enabled = false
# Subsurface scattering
if get_source(INPUT_SSS) != null:
m.subsurf_scatter_enabled = true
m.subsurf_scatter_strength = parameters.sss
m.subsurf_scatter_texture = get_generated_texture("sss", file_prefix)
else:
m.subsurf_scatter_enabled = false
else:
m.set_shader_param("albedo", parameters.albedo_color)
m.set_shader_param("texture_albedo", get_generated_texture("albedo", file_prefix))
m.set_shader_param("metallic", parameters.metallic)
m.set_shader_param("texture_metallic", get_generated_texture("orm", file_prefix))
m.set_shader_param("metallic_texture_channel", PoolRealArray([0.0, 0.0, 1.0, 0.0]))
m.set_shader_param("roughness", parameters.roughness)
m.set_shader_param("texture_roughness", get_generated_texture("orm", file_prefix))
m.set_shader_param("roughness_texture_channel", PoolRealArray([0.0, 1.0, 0.0, 0.0]))
m.set_shader_param("emission_energy", parameters.emission_energy)
m.set_shader_param("texture_emission", get_generated_texture("emission", file_prefix))
m.set_shader_param("normal_scale", parameters.normal_scale)
m.set_shader_param("texture_normal", get_generated_texture("normal", file_prefix))
m.set_shader_param("depth_scale", parameters.depth_scale * 0.2)
m.set_shader_param("texture_depth", get_generated_texture("depth", file_prefix))
# Export
func get_export_profiles() -> Array:
return shader_model.exports.keys()
func get_export_extension(profile : String) -> String:
return shader_model.exports[profile].export_extension
func get_export_path(profile : String) -> String:
if export_paths.has(profile):
return export_paths[profile]
return ""
func subst_string(s : String, export_context : Dictionary) -> String:
var modified : bool = true
while modified:
modified = false
for k in export_context.keys():
var new_s = s.replace(k, export_context[k])
if new_s != s:
s = new_s
modified = true
while (true):
var search_string = "$(expr:"
var position = s.find(search_string)
if position == -1:
break
var parenthesis_level = 0
var expr_begin = position+search_string.length()
for i in range(expr_begin, s.length()):
if s[i] == '(':
parenthesis_level += 1
elif s[i] == ')':
if parenthesis_level == 0:
var expression = s.substr(expr_begin, i-expr_begin)
var expr = Expression.new()
var error = expr.parse(expression, [])
if error == OK:
s = s.replace(s.substr(position, i+1-position), str(expr.execute()))
else:
s = s.replace(s.substr(position, i+1-position), "EXPRESSION ERROR ("+expression+")")
break
parenthesis_level -= 1
return s
func create_file_from_template(template : String, file_name : String, export_context : Dictionary) -> bool:
var in_file = File.new()
var out_file = File.new()
if in_file.open(mm_loader.STD_GENDEF_PATH+"/"+template, File.READ) != OK:
if in_file.open(OS.get_executable_path().get_base_dir()+"/generators/"+template, File.READ) != OK:
print("Cannot find template file "+template)
return false
Directory.new().remove(file_name)
if out_file.open(file_name, File.WRITE) != OK:
print("Cannot write file '"+file_name+"' ("+str(out_file.get_error())+")")
return false
var skip_state : Array = [ false ]
while ! in_file.eof_reached():
var l = in_file.get_line()
if l.left(4) == "$if ":
var condition = subst_string(l.right(4), export_context)
var expr = Expression.new()
var error = expr.parse(condition, [])
if error != OK:
print("Error in expression "+condition+": "+expr.get_error_text())
continue
skip_state.push_back(!expr.execute())
elif l.left(3) == "$fi":
skip_state.pop_back()
elif l.left(5) == "$else":
skip_state.push_back(!skip_state.pop_back())
elif ! skip_state.back():
out_file.store_line(subst_string(l, export_context))
return true
func export_material(prefix, profile) -> void:
export_paths[profile] = prefix
var export_context : Dictionary = {
"$(path_prefix)":prefix,
"$(file_prefix)":prefix.get_file()
}
for i in range(shader_model.inputs.size()):
var input = shader_model.inputs[i]
export_context["$(connected:"+input.name+")"] = "true" if get_source(i) != null else "false"
for p in shader_model.parameters:
var value = p.default
if parameters.has(p.name):
value = parameters[p.name]
match p.type:
"float", "size":
export_context["$(param:"+p.name+")"] = str(value)
"color":
export_context["$(param:"+p.name+".r)"] = str(value.r)
export_context["$(param:"+p.name+".g)"] = str(value.g)
export_context["$(param:"+p.name+".b)"] = str(value.b)
export_context["$(param:"+p.name+".a)"] = str(value.a)
_:
print(p.type+" not supported in material")
if shader_model.exports[profile].has("uids"):
for i in range(shader_model.exports[profile].uids):
var uid : String
var r = []
for k in range(16):
r.append(randi() & 255)
r[6] = (r[6] & 0x0f) | 0x40
r[8] = (r[8] & 0x3f) | 0x80
for k in range(16):
uid += '%02x' % r[k]
export_context["$(uid:"+str(i)+")"] = uid
for f in shader_model.exports[profile].files:
if f.has("conditions"):
var condition = subst_string(f.conditions, export_context)
var expr = Expression.new()
var error = expr.parse(condition, [])
if error != OK:
print("Error in expression: "+expr.get_error_text())
continue
if !expr.execute():
continue
match f.type:
"texture":
var file_name = subst_string(f.file_name, export_context)
var result = render(f.output, get_image_size())
while result is GDScriptFunctionState:
result = yield(result, "completed")
result.save_to_file(file_name)
result.release()
"template":
var file_export_context = export_context.duplicate()
if f.has("file_params"):
for p in f.file_params.keys():
file_export_context["$(file_param:"+p+")"] = f.file_params[p]
var file_name = subst_string(f.file_name, export_context)
create_file_from_template(f.template, file_name, file_export_context)
func _serialize_data(data: Dictionary) -> Dictionary:
data = ._serialize_data(data)
data.export_paths = export_paths
return data
func _deserialize(data : Dictionary) -> void:
._deserialize(data)
if data.has("export_paths"):
export_paths = data.export_paths.duplicate()