Merge pull request #2239 from QuantumCoderQC/MatParamPerObject

Set material parameter on per object basis
This commit is contained in:
Lubos Lenco 2021-06-26 12:17:40 +02:00 committed by GitHub
commit 616a0e230d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 510 additions and 86 deletions

View file

@ -1,40 +1,38 @@
package armory.logicnode;
import iron.Scene;
import iron.data.MaterialData;
import iron.object.Object;
import armory.trait.internal.UniformsManager;
class SetMaterialImageParamNode extends LogicNode {
static var registered = false;
static var map = new Map<MaterialData, Map<String, kha.Image>>();
public function new(tree: LogicTree) {
super(tree);
if (!registered) {
registered = true;
iron.object.Uniforms.externalTextureLinks.push(textureLink);
}
}
override function run(from: Int) {
var mat = inputs[1].get();
if (mat == null) return;
var entry = map.get(mat);
if (entry == null) {
entry = new Map();
map.set(mat, entry);
var object = inputs[1].get();
if(object == null) return;
var perObject = inputs[2].get();
if(perObject == null) perObject = false;
var mat = inputs[3].get();
if(mat == null) return;
if(! perObject){
UniformsManager.removeObjectFromMap(object, Texture);
object = Scene.active.root;
}
iron.data.Data.getImage(inputs[3].get(), function(image: kha.Image) {
entry.set(inputs[2].get(), image); // Node name, value
var img = inputs[5].get();
if(img == null) return;
iron.data.Data.getImage(img, function(image: kha.Image) {
UniformsManager.setTextureValue(mat, object, inputs[4].get(), image);
});
runOutput(0);
}
static function textureLink(object: Object, mat: MaterialData, link: String): kha.Image {
if (mat == null) return null;
var entry = map.get(mat);
if (entry == null) return null;
return entry.get(link);
}
}

View file

@ -1,38 +1,33 @@
package armory.logicnode;
import iron.Scene;
import iron.math.Vec4;
import iron.data.MaterialData;
import iron.object.Object;
import armory.trait.internal.UniformsManager;
class SetMaterialRgbParamNode extends LogicNode {
static var registered = false;
static var map = new Map<MaterialData, Map<String, Vec4>>();
public function new(tree: LogicTree) {
super(tree);
if (!registered) {
registered = true;
iron.object.Uniforms.externalVec3Links.push(vec3Link);
}
}
override function run(from: Int) {
var mat = inputs[1].get();
if (mat == null) return;
var entry = map.get(mat);
if (entry == null) {
entry = new Map();
map.set(mat, entry);
var object = inputs[1].get();
if(object == null) return;
var perObject = inputs[2].get();
if(perObject == null) perObject = false;
var mat = inputs[3].get();
if(mat == null) return;
if(! perObject){
UniformsManager.removeObjectFromMap(object, Vector);
object = Scene.active.root;
}
entry.set(inputs[2].get(), inputs[3].get()); // Node name, value
UniformsManager.setVec3Value(mat, object, inputs[4].get(), inputs[5].get());
runOutput(0);
}
static function vec3Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 {
if (mat == null) return null;
var entry = map.get(mat);
if (entry == null) return null;
return entry.get(link);
}
}

View file

@ -1,37 +1,33 @@
package armory.logicnode;
import iron.Scene;
import iron.data.MaterialData;
import iron.object.Object;
import armory.trait.internal.UniformsManager;
class SetMaterialValueParamNode extends LogicNode {
static var registered = false;
static var map = new Map<MaterialData, Map<String, Null<kha.FastFloat>>>();
public function new(tree: LogicTree) {
super(tree);
if (!registered) {
registered = true;
iron.object.Uniforms.externalFloatLinks.push(floatLink);
}
}
override function run(from: Int) {
var mat = inputs[1].get();
if (mat == null) return;
var entry = map.get(mat);
if (entry == null) {
entry = new Map();
map.set(mat, entry);
var object = inputs[1].get();
if(object == null) return;
var perObject = inputs[2].get();
if(perObject == null) perObject = false;
var mat = inputs[3].get();
if(mat == null) return;
if(! perObject){
UniformsManager.removeObjectFromMap(object, Float);
object = Scene.active.root;
}
entry.set(inputs[2].get(), inputs[3].get()); // Node name, value
UniformsManager.setFloatValue(mat, object, inputs[4].get(), inputs[5].get());
runOutput(0);
}
static function floatLink(object: Object, mat: MaterialData, link: String): Null<kha.FastFloat> {
if (mat == null) return null;
var entry = map.get(mat);
if (entry == null) return null;
return entry.get(link);
}
}

View file

@ -0,0 +1,321 @@
package armory.trait.internal;
import iron.object.MeshObject;
import iron.Trait;
import kha.Image;
import iron.math.Vec4;
import iron.data.MaterialData;
import iron.Scene;
import iron.object.Object;
import iron.object.Uniforms;
class UniformsManager extends Trait{
static var floatsRegistered = false;
static var floatsMap = new Map<Object, Map<MaterialData, Map<String, Null<kha.FastFloat>>>>();
static var vectorsRegistered = false;
static var vectorsMap = new Map<Object, Map<MaterialData, Map<String, Vec4>>>();
static var texturesRegistered = false;
static var texturesMap = new Map<Object, Map<MaterialData, Map<String, kha.Image>>>();
static var sceneRemoveInitalized = false;
public var unifromExists = false;
public function new(){
super();
notifyOnInit(init);
notifyOnRemove(removeObject);
if(! sceneRemoveInitalized){
Scene.active.notifyOnRemove(removeScene);
}
}
function init() {
var materials = cast(object, MeshObject).materials;
for (material in materials){
var exists = registerShaderUniforms(material);
if(exists) {
unifromExists = true;
}
}
if(! unifromExists) {
this.remove();
}
}
static function removeScene(){
removeObjectFromAllMaps(Scene.active.root);
}
function removeObject() {
removeObjectFromAllMaps(object);
}
// Helper method to register float, vec3 and texture getter functions
static function register(type: UniformType){
switch (type){
case Float:{
if(! floatsRegistered){
floatsRegistered = true;
Uniforms.externalFloatLinks.push(floatLink);
}
}
case Vector:{
if(! vectorsRegistered){
vectorsRegistered = true;
Uniforms.externalVec3Links.push(vec3Link);
}
}
case Texture:{
if(! texturesRegistered){
texturesRegistered = true;
Uniforms.externalTextureLinks.push(textureLink);
}
}
}
}
// Register and map shader uniforms if it is an armory shader parameter
public static function registerShaderUniforms(material: MaterialData) : Bool {
var unifromExist = false;
if(! floatsMap.exists(Scene.active.root)) floatsMap.set(Scene.active.root, null);
if(! vectorsMap.exists(Scene.active.root)) vectorsMap.set(Scene.active.root, null);
if(! texturesMap.exists(Scene.active.root)) texturesMap.set(Scene.active.root, null);
for(context in material.shader.raw.contexts){ // For each context in shader
for (constant in context.constants){ // For each constant in the context
if(constant.is_arm_parameter){ // Chack if armory parameter
unifromExist = true;
var object = Scene.active.root; // Map default uniforms to scene root
switch (constant.type){
case "float":{
var link = constant.link;
var value:Float = constant.float;
setFloatValue(material, object, link, value);
register(Float);
}
case "vec3":{
var vec = new Vec4();
vec.x = constant.vec3.get(0);
vec.y = constant.vec3.get(1);
vec.z = constant.vec3.get(2);
setVec3Value(material, object, constant.link, vec);
register(Vector);
}
}
}
}
for (texture in context.texture_units){
if(texture.is_arm_parameter){ // Chack if armory parameter
unifromExist = true;
var object = Scene.active.root; // Map default texture to scene root
iron.data.Data.getImage(texture.default_image_file, function(image: kha.Image) {
setTextureValue(material, object, texture.link, image);
});
register(Texture);
}
}
}
return unifromExist;
}
// Method to set map Object -> Material -> Link -> FLoat
public static function setFloatValue(material: MaterialData, object: Object, link: String, value: Null<kha.FastFloat>){
if(object == null || material == null || link == null) return;
var map = floatsMap;
var matMap = map.get(object);
if (matMap == null) {
matMap = new Map();
map.set(object, matMap);
}
var entry = matMap.get(material);
if (entry == null) {
entry = new Map();
matMap.set(material, entry);
}
entry.set(link, value); // parameter name, value
}
// Method to set map Object -> Material -> Link -> Vec3
public static function setVec3Value(material: MaterialData, object: Object, link: String, value: Vec4){
if(object == null || material == null || link == null) return;
var map = vectorsMap;
var matMap = map.get(object);
if (matMap == null) {
matMap = new Map();
map.set(object, matMap);
}
var entry = matMap.get(material);
if (entry == null) {
entry = new Map();
matMap.set(material, entry);
}
entry.set(link, value); // parameter name, value
}
// Method to set map Object -> Material -> Link -> Texture
public static function setTextureValue(material: MaterialData, object: Object, link: String, value: kha.Image){
if(object == null || material == null || link == null) return;
var map = texturesMap;
var matMap = map.get(object);
if (matMap == null) {
matMap = new Map();
map.set(object, matMap);
}
var entry = matMap.get(material);
if (entry == null) {
entry = new Map();
matMap.set(material, entry);
}
entry.set(link, value); // parameter name, value
}
// Mehtod to get object specific material parameter float value
static function floatLink(object: Object, mat: MaterialData, link: String): Null<kha.FastFloat> {
if(object == null || mat == null) return null;
if(! floatsMap.exists(object)){
object = Scene.active.root;
}
var material = floatsMap.get(object);
if (material == null) return null;
var entry = material.get(mat);
if (entry == null) return null;
return entry.get(link);
}
// Mehtod to get object specific material parameter vec3 value
static function vec3Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 {
if(object == null || mat == null) return null;
if(! vectorsMap.exists(object)){
object = Scene.active.root;
}
var material = vectorsMap.get(object);
if (material == null) return null;
var entry = material.get(mat);
if (entry == null) return null;
return entry.get(link);
}
// Mehtod to get object specific material parameter texture value
static function textureLink(object: Object, mat: MaterialData, link: String): kha.Image {
if(object == null || mat == null) return null;
if(! texturesMap.exists(object)){
object = Scene.active.root;
}
var material = texturesMap.get(object);
if (material == null) return null;
var entry = material.get(mat);
if (entry == null) return null;
return entry.get(link);
}
// Returns complete map of float value material paramets
public static function getFloatsMap():Map<Object, Map<MaterialData, Map<String, Null<kha.FastFloat>>>>{
return floatsMap;
}
// Returns complete map of vec3 value material paramets
public static function getVectorsMap():Map<Object, Map<MaterialData, Map<String, Vec4>>>{
return vectorsMap;
}
// Returns complete map of texture value material paramets
public static function getTexturesMap():Map<Object, Map<MaterialData, Map<String, kha.Image>>>{
return texturesMap;
}
// Remove all object specific material paramenter keys
public static function removeObjectFromAllMaps(object: Object) {
floatsMap.remove(object);
vectorsMap.remove(object);
texturesMap.remove(object);
}
// Remove object specific material paramenter keys
public static function removeObjectFromMap(object: Object, type: UniformType) {
switch (type){
case Float: floatsMap.remove(object);
case Vector: vectorsMap.remove(object);
case Texture: texturesMap.remove(object);
}
}
}
@:enum abstract UniformType(Int) from Int to Int {
var Float = 0;
var Vector = 1;
var Texture = 2;
}

View file

@ -2460,6 +2460,13 @@ Make sure the mesh only has tris/quads.""")
else:
self.material_to_object_dict[mat] = [bobject]
self.material_to_arm_object_dict[mat] = [o]
# Add UniformsManager trait
if type is NodeType.MESH:
uniformManager = {}
uniformManager['type'] = 'Script'
uniformManager['class_name'] = 'armory.trait.internal.UniformsManager'
o['traits'].append(uniformManager)
# Export constraints
if len(bobject.constraints) > 0:

View file

@ -1,17 +1,43 @@
from arm.logicnode.arm_nodes import *
class SetMaterialImageParamNode(ArmLogicTreeNode):
"""TO DO."""
"""Set an image value material parameter to the specified object.
@seeNode Get Scene Root
@input Object: Object whose material parameter should change. Use `Get Scene Root` node to set parameter globally.
@input Per Object:
- `Enabled`: Set material parameter specific to this object. Global parameter will be ignored.
- `Disabled`: Set parameter globally, including this object.
@input Material: Material whose parameter to be set.
@input Node: Name of the parameter.
@input Image: Name of the image.
"""
bl_idname = 'LNSetMaterialImageParamNode'
bl_label = 'Set Material Image Param'
arm_section = 'params'
arm_version = 1
arm_version = 2
def init(self, context):
super(SetMaterialImageParamNode, self).init(context)
self.add_input('ArmNodeSocketAction', 'In')
self.add_input('ArmNodeSocketObject', 'Object')
self.add_input('NodeSocketBool', 'Per Object')
self.add_input('NodeSocketShader', 'Material')
self.add_input('NodeSocketString', 'Node')
self.add_input('NodeSocketString', 'Image')
self.add_output('ArmNodeSocketAction', 'Out')
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.arm_version not in (0, 1):
raise LookupError()
return NodeReplacement(
'LNSetMaterialImageParamNode', self.arm_version, 'LNSetMaterialImageParamNode', 2,
in_socket_mapping={0:0, 1:3, 2:4, 3:5}, out_socket_mapping={0:0}
)

View file

@ -1,17 +1,43 @@
from arm.logicnode.arm_nodes import *
class SetMaterialRgbParamNode(ArmLogicTreeNode):
"""TO DO."""
"""Set a color or vector value material parameter to the specified object.
@seeNode Get Scene Root
@input Object: Object whose material parameter should change. Use `Get Scene Root` node to set parameter globally.
@input Per Object:
- `Enabled`: Set material parameter specific to this object. Global parameter will be ignored.
- `Disabled`: Set parameter globally, including this object.
@input Material: Material whose parameter to be set.
@input Node: Name of the parameter.
@input Color: Color or vector input.
"""
bl_idname = 'LNSetMaterialRgbParamNode'
bl_label = 'Set Material RGB Param'
arm_section = 'params'
arm_version = 1
arm_version = 2
def init(self, context):
super(SetMaterialRgbParamNode, self).init(context)
self.add_input('ArmNodeSocketAction', 'In')
self.add_input('ArmNodeSocketObject', 'Object')
self.add_input('NodeSocketBool', 'Per Object')
self.add_input('NodeSocketShader', 'Material')
self.add_input('NodeSocketString', 'Node')
self.add_input('NodeSocketColor', 'Color')
self.add_output('ArmNodeSocketAction', 'Out')
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.arm_version not in (0, 1):
raise LookupError()
return NodeReplacement(
'LNSetMaterialRgbParamNode', self.arm_version, 'LNSetMaterialRgbParamNode', 2,
in_socket_mapping={0:0, 1:3, 2:4, 3:5}, out_socket_mapping={0:0}
)

View file

@ -1,17 +1,44 @@
from arm.logicnode.arm_nodes import *
class SetMaterialValueParamNode(ArmLogicTreeNode):
"""TO DO."""
"""Set a float value material parameter to the specified object.
@seeNode Get Scene Root
@input Object: Object whose material parameter should change. Use `Get Scene Root` node to set parameter globally.
@input Per Object:
- `Enabled`: Set material parameter specific to this object. Global parameter will be ignored.
- `Disabled`: Set parameter globally, including this object.
@input Material: Material whose parameter to be set.
@input Node: Name of the parameter.
@input Float: float value.
"""
bl_idname = 'LNSetMaterialValueParamNode'
bl_label = 'Set Material Value Param'
arm_section = 'params'
arm_version = 1
arm_version = 2
def init(self, context):
super(SetMaterialValueParamNode, self).init(context)
self.add_input('ArmNodeSocketAction', 'In')
self.add_input('ArmNodeSocketObject', 'Object')
self.add_input('NodeSocketBool', 'Per Object')
self.add_input('NodeSocketShader', 'Material')
self.add_input('NodeSocketString', 'Node')
self.add_input('NodeSocketFloat', 'Float')
self.add_output('ArmNodeSocketAction', 'Out')
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.arm_version not in (0, 1):
raise LookupError()
return NodeReplacement(
'LNSetMaterialValueParamNode', self.arm_version, 'LNSetMaterialValueParamNode', 2,
in_socket_mapping={0:0, 1:3, 2:4, 3:5}, out_socket_mapping={0:0}
)

View file

@ -560,7 +560,7 @@ def to_uniform(inp: bpy.types.NodeSocket):
def store_var_name(node: bpy.types.Node):
return node_name(node.name) + '_store'
def texture_store(node, tex, tex_name, to_linear=False, tex_link=None):
def texture_store(node, tex, tex_name, to_linear=False, tex_link=None, default_value=None, is_arm_mat_param=None):
curshader = state.curshader
tex_store = store_var_name(node)
@ -569,7 +569,7 @@ def texture_store(node, tex, tex_name, to_linear=False, tex_link=None):
state.parsed.add(tex_store)
mat_bind_texture(tex)
state.con.add_elem('tex', 'short2norm')
curshader.add_uniform('sampler2D {0}'.format(tex_name), link=tex_link)
curshader.add_uniform('sampler2D {0}'.format(tex_name), link=tex_link, default_value=default_value, is_arm_mat_param=is_arm_mat_param)
triplanar = node.projection == 'BOX'
if node.inputs[0].is_linked:
uv_name = parse_vector_input(node.inputs[0])

View file

@ -92,7 +92,13 @@ def parse_attribute(node: bpy.types.ShaderNodeAttribute, out_socket: bpy.types.N
def parse_rgb(node: bpy.types.ShaderNodeRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
if node.arm_material_param:
nn = 'param_' + c.node_name(node.name)
state.curshader.add_uniform(f'vec3 {nn}', link=f'{node.name}')
v = out_socket.default_value
value = []
value.append(float(v[0]))
value.append(float(v[1]))
value.append(float(v[2]))
is_arm_mat_param = True
state.curshader.add_uniform(f'vec3 {nn}', link=f'{node.name}', default_value = value, is_arm_mat_param = is_arm_mat_param)
return nn
else:
return c.to_vec3(out_socket.default_value)
@ -351,7 +357,9 @@ def parse_lightpath(node: bpy.types.ShaderNodeLightPath, out_socket: bpy.types.N
def parse_value(node: bpy.types.ShaderNodeValue, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr:
if node.arm_material_param:
nn = 'param_' + c.node_name(node.name)
state.curshader.add_uniform('float {0}'.format(nn), link='{0}'.format(node.name))
value = c.to_vec1(node.outputs[0].default_value)
is_arm_mat_param = True
state.curshader.add_uniform('float {0}'.format(nn), link='{0}'.format(node.name), default_value=value, is_arm_mat_param=is_arm_mat_param)
return nn
else:
return c.to_vec1(node.outputs[0].default_value)

View file

@ -114,15 +114,22 @@ def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.No
tex_name = c.node_name(node.name)
tex = c.make_texture(node, tex_name)
tex_link = node.name if node.arm_material_param else None
tex_link = None
tex_default_file = None
is_arm_mat_param = None
if node.arm_material_param:
tex_link = node.name
is_arm_mat_param = True
if tex['file'] is not None:
tex_default_file = tex['file']
if tex is not None:
state.curshader.write_textures += 1
if use_color_out:
to_linear = node.image is not None and node.image.colorspace_settings.name == 'sRGB'
res = f'{c.texture_store(node, tex, tex_name, to_linear, tex_link=tex_link)}.rgb'
res = f'{c.texture_store(node, tex, tex_name, to_linear, tex_link=tex_link, default_value=tex_default_file, is_arm_mat_param=is_arm_mat_param)}.rgb'
else:
res = f'{c.texture_store(node, tex, tex_name, tex_link=tex_link)}.a'
res = f'{c.texture_store(node, tex, tex_name, tex_link=tex_link, default_value=tex_default_file, is_arm_mat_param=is_arm_mat_param)}.a'
state.curshader.write_textures -= 1
return res

View file

@ -109,19 +109,27 @@ class ShaderContext:
def get(self):
return self.data
def add_constant(self, ctype, name, link=None):
def add_constant(self, ctype, name, link=None, default_value=None, is_arm_mat_param=None):
for c in self.constants:
if c['name'] == name:
return
c = { 'name': name, 'type': ctype }
if link != None:
c = { 'name': name, 'type': ctype}
if link is not None:
c['link'] = link
if default_value is not None:
if ctype == 'float':
c['float'] = default_value
if ctype == 'vec3':
c['vec3'] = default_value
if is_arm_mat_param is not None:
c['is_arm_parameter'] = 'true'
self.constants.append(c)
def add_texture_unit(self, name, link=None, is_image=None,
addr_u=None, addr_v=None,
filter_min=None, filter_mag=None, mipmap_filter=None):
filter_min=None, filter_mag=None, mipmap_filter=None,
default_value=None, is_arm_mat_param=None):
for c in self.tunits:
if c['name'] == name:
return
@ -141,6 +149,10 @@ class ShaderContext:
c['filter_mag'] = filter_mag
if mipmap_filter is not None:
c['mipmap_filter'] = mipmap_filter
if default_value is not None:
c['default_image_file'] = default_value
if is_arm_mat_param is not None:
c['is_arm_parameter'] = 'true'
self.tunits.append(c)
@ -238,7 +250,7 @@ class Shader:
def add_uniform(self, s, link=None, included=False, top=False,
tex_addr_u=None, tex_addr_v=None,
tex_filter_min=None, tex_filter_mag=None,
tex_mipmap_filter=None):
tex_mipmap_filter=None, default_value=None, is_arm_mat_param=None):
ar = s.split(' ')
# layout(RGBA8) image3D voxels
utype = ar[-2]
@ -257,7 +269,8 @@ class Shader:
self.context.add_texture_unit(
uname, link, is_image,
tex_addr_u, tex_addr_v,
tex_filter_min, tex_filter_mag, tex_mipmap_filter)
tex_filter_min, tex_filter_mag, tex_mipmap_filter,
default_value=default_value, is_arm_mat_param=is_arm_mat_param)
else:
# Prefer vec4[] for d3d to avoid padding
if ar[0] == 'float' and '[' in ar[1]:
@ -266,7 +279,7 @@ class Shader:
elif ar[0] == 'vec4' and '[' in ar[1]:
ar[0] = 'floats'
ar[1] = ar[1].split('[', 1)[0]
self.context.add_constant(ar[0], ar[1], link=link)
self.context.add_constant(ar[0], ar[1], link=link, default_value=default_value, is_arm_mat_param=is_arm_mat_param)
if top:
if not included and s not in self.uniforms_top:
self.uniforms_top.append(s)