Merge pull request #2375 from QuantumCoderQC/shape-keys

Implementing Shape keys
This commit is contained in:
Lubos Lenco 2021-11-06 12:31:23 +01:00 committed by GitHub
commit 263d5853a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 464 additions and 83 deletions

View File

@ -0,0 +1,53 @@
uniform sampler2D morphDataPos;
uniform sampler2D morphDataNor;
uniform vec2 morphScaleOffset;
uniform vec2 morphDataDim;
uniform vec4 morphWeights[8];
void getMorphedVertex(vec2 uvCoord, inout vec3 A){
for(int i = 0; i<8; i++ )
{
vec4 tempCoordY = vec4( uvCoord.y - (i * 4) * morphDataDim.y,
uvCoord.y - (i * 4 + 1) * morphDataDim.y,
uvCoord.y - (i * 4 + 2) * morphDataDim.y,
uvCoord.y - (i * 4 + 3) * morphDataDim.y);
vec3 morph = texture(morphDataPos, vec2(uvCoord.x, tempCoordY.x)).rgb * morphScaleOffset.x + morphScaleOffset.y;
A += morphWeights[i].x * morph;
morph = texture(morphDataPos, vec2(uvCoord.x, tempCoordY.y)).rgb * morphScaleOffset.x + morphScaleOffset.y;
A += morphWeights[i].y * morph;
morph = texture(morphDataPos, vec2(uvCoord.x, tempCoordY.z)).rgb * morphScaleOffset.x + morphScaleOffset.y;
A += morphWeights[i].z * morph;
morph = texture(morphDataPos, vec2(uvCoord.x, tempCoordY.w)).rgb * morphScaleOffset.x + morphScaleOffset.y;
A += morphWeights[i].w * morph;
}
}
void getMorphedNormal(vec2 uvCoord, vec3 oldNor, inout vec3 morphNor){
for(int i = 0; i<8; i++ )
{
vec4 tempCoordY = vec4( uvCoord.y - (i * 4) * morphDataDim.y,
uvCoord.y - (i * 4 + 1) * morphDataDim.y,
uvCoord.y - (i * 4 + 2) * morphDataDim.y,
uvCoord.y - (i * 4 + 3) * morphDataDim.y);
vec3 norm = oldNor + morphWeights[i].x * (texture(morphDataNor, vec2(uvCoord.x, tempCoordY.x)).rgb * 2.0 - 1.0);
morphNor += norm;
norm = oldNor + morphWeights[i].y * (texture(morphDataNor, vec2(uvCoord.x, tempCoordY.y)).rgb * 2.0 - 1.0);
morphNor += norm;
norm = oldNor + morphWeights[i].z * (texture(morphDataNor, vec2(uvCoord.x, tempCoordY.z)).rgb * 2.0 - 1.0);
morphNor += norm;
norm = oldNor + morphWeights[i].w * (texture(morphDataNor, vec2(uvCoord.x, tempCoordY.w)).rgb * 2.0 - 1.0);
morphNor += norm;
}
morphNor = normalize(morphNor);
}

View File

@ -0,0 +1,25 @@
package armory.logicnode;
import iron.object.MeshObject;
class SetObjectShapeKeyNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
#if arm_morph_target
var object: Dynamic = inputs[1].get();
var shapeKey: String = inputs[2].get();
var value: Dynamic = inputs[3].get();
assert(Error, object != null, "Object should not be null");
var morph = cast(object, MeshObject).morphTarget;
assert(Error, morph != null, "Object does not have shape keys");
morph.setMorphValue(shapeKey, value);
#end
runOutput(0);
}
}

View File

@ -22,6 +22,8 @@ import numpy as np
import bpy
from mathutils import *
import bmesh
import arm.assets as assets
import arm.exporter_opt as exporter_opt
import arm.log as log
@ -196,14 +198,30 @@ class ArmoryExporter:
@staticmethod
def get_shape_keys(mesh):
rpdat = arm.utils.get_rp()
if(rpdat.arm_morph_target != 'On'):
return False
# Metaball
if not hasattr(mesh, 'shape_keys'):
return None
return False
shape_keys = mesh.shape_keys
if shape_keys and len(shape_keys.key_blocks) > 1:
return shape_keys
return None
if not shape_keys:
return False
if len(shape_keys.key_blocks) < 2:
return False
for shape_key in shape_keys.key_blocks[1:]:
if(not shape_key.mute):
return True
return False
@staticmethod
def get_morph_uv_index(mesh):
i = 0
for uv_layer in mesh.uv_layers:
if uv_layer.name == 'UVMap_shape_key':
return i
i +=1
def find_bone(self, name: str) -> Optional[Tuple[bpy.types.Bone, Dict]]:
"""Finds the bone reference (a tuple containing the bone object
@ -1114,6 +1132,177 @@ class ArmoryExporter:
if 'constraints' not in oskin:
oskin['constraints'] = []
self.add_constraints(bone, oskin, bone=True)
def export_shape_keys(self, bobject: bpy.types.Object, export_mesh: bpy.types.Mesh, out_mesh):
# Max shape keys supported
max_shape_keys = 32
# Path to store shape key textures
output_dir = bpy.path.abspath('//') + "MorphTargets\\"
name = bobject.data.name
vert_pos = []
vert_nor = []
names = []
default_values = [0] * max_shape_keys
# Shape key base mesh
shape_key_base = bobject.data.shape_keys.key_blocks[0]
count = 0
# Loop through all shape keys
for shape_key in bobject.data.shape_keys.key_blocks[1:]:
if(count > max_shape_keys - 1):
break
# get vertex data from shape key
if shape_key.mute:
continue
vert_data = self.get_vertex_data_from_shape_key(shape_key_base, shape_key)
vert_pos.append(vert_data['pos'])
vert_nor.append(vert_data['nor'])
names.append(shape_key.name)
default_values[count] = shape_key.value
count += 1
# No shape keys present or all shape keys are muted
if (count < 1):
return
# Convert to array for easy manipulation
pos_array = np.array(vert_pos)
nor_array = np.array(vert_nor)
# Min and Max values of shape key displacements
max = np.amax(pos_array)
min = np.amin(pos_array)
array_size = len(pos_array[0]), len(pos_array)
# Get best 2^n image size to fit shape key data (min = 2 X 2, max = 4096 X 4096)
img_size, extra_zeros, block_size = self.get_best_image_size(array_size)
# Image size required is too large. Skip export
if(img_size < 1):
log.error(f"""object {bobject.name} contains too many vertices or shape keys to support shape keys export""")
self.remove_morph_uv_set(bobject)
return
# Write data to image
self.bake_to_image(pos_array, nor_array, max, min, extra_zeros, img_size, name, output_dir)
# Create a new UV set for shape keys
self.create_morph_uv_set(bobject, img_size)
# Export Shape Key names, defaults, etc..
morph_target = {}
morph_target['morph_target_data_file'] = name
morph_target['morph_target_ref'] = names
morph_target['morph_target_defaults'] = default_values
morph_target['num_morph_targets'] = count
morph_target['morph_scale'] = max - min
morph_target['morph_offset'] = min
morph_target['morph_img_size'] = img_size
morph_target['morph_block_size'] = block_size
out_mesh['morph_target'] = morph_target
return
def get_vertex_data_from_shape_key(self, shape_key_base, shape_key_data):
base_vert_pos = shape_key_base.data.values()
base_vert_nor = shape_key_base.normals_split_get()
vert_pos = shape_key_data.data.values()
vert_nor = shape_key_data.normals_split_get()
num_verts = len(vert_pos)
pos = []
nor = []
# Loop through all vertices
for i in range(num_verts):
# Vertex position relative to base vertex
pos.append(list(vert_pos[i].co - base_vert_pos[i].co))
temp = []
for j in range(3):
# Vertex normal relative to base vertex
temp.append(vert_nor[j + i * 3] - base_vert_nor[j + i * 3])
nor.append(temp)
return {'pos': pos, 'nor': nor}
def bake_to_image(self, pos_array, nor_array, pos_max, pos_min, extra_x, img_size, name, output_dir):
# Scale position data between [0, 1] to bake to image
pos_array_scaled = np.interp(pos_array, (pos_min, pos_max), (0, 1))
# Write positions to image
self.write_output_image(pos_array_scaled, extra_x, img_size, name + '_morph_pos', output_dir)
# Scale normal data between [0, 1] to bake to image
nor_array_scaled = np.interp(nor_array, (-1, 1), (0, 1))
# Write normals to image
self.write_output_image(nor_array_scaled, extra_x, img_size, name + '_morph_nor', output_dir)
def write_output_image(self, data, extra_x, img_size, name, output_dir):
# Pad data with zeros to make up for required number of pixels of 2^n format
data = np.pad(data, ((0, 0), (0, extra_x), (0, 0)), 'minimum')
pixel_list = []
for y in range(len(data)):
for x in range(len(data[0])):
# assign RGBA
pixel_list.append(data[y, x, 0])
pixel_list.append(data[y, x, 1])
pixel_list.append(data[y, x, 2])
pixel_list.append(1.0)
pixel_list = (pixel_list + [0] * (img_size * img_size * 4 - len(pixel_list)))
image = bpy.data.images.new(name, width = img_size, height = img_size, is_data = True)
image.pixels = pixel_list
image.save_render(output_dir + name + ".png", scene= bpy.context.scene)
bpy.data.images.remove(image)
def get_best_image_size(self, size):
for i in range(1, 12):
block_len = pow(2, i)
block_height = np.ceil(size[0]/block_len)
if(block_height * size[1] <= block_len):
extra_zeros_x = block_height * block_len - size[0]
return pow(2,i), round(extra_zeros_x), block_height
return 0, 0, 0
def remove_morph_uv_set(self, obj):
layer = obj.data.uv_layers.get('UVMap_shape_key')
if(layer is not None):
obj.data.uv_layers.remove(layer)
def create_morph_uv_set(self, obj, img_size):
# Get/ create morph UV set
if(obj.data.uv_layers.get('UVMap_shape_key') is None):
obj.data.uv_layers.new(name = 'UVMap_shape_key')
bm = bmesh.new()
bm.from_mesh(obj.data)
uv_layer = bm.loops.layers.uv.get('UVMap_shape_key')
pixel_size = 1.0 / img_size
i = 0
j = 0
# Arrange UVs to match exported image pixels
for v in bm.verts:
for l in v.link_loops:
uv_data = l[uv_layer]
uv_data.uv = Vector(((i + 0.5) * pixel_size, (j + 0.5) * pixel_size))
i += 1
if(i > img_size - 1):
j += 1
i = 0
bm.to_mesh(obj.data)
bm.free()
def write_mesh(self, bobject: bpy.types.Object, fp, out_mesh):
if bpy.data.worlds['Arm'].arm_single_data_file:
@ -1142,43 +1331,65 @@ class ArmoryExporter:
num_verts = len(loops)
num_uv_layers = len(exportMesh.uv_layers)
is_baked = self.has_baked_material(bobject, exportMesh.materials)
has_tex = (self.get_export_uvs(bobject.data) and num_uv_layers > 0) or is_baked
has_tex1 = has_tex and num_uv_layers > 1
num_colors = len(exportMesh.vertex_colors)
has_col = self.get_export_vcols(bobject.data) and num_colors > 0
# Check if shape keys were exported
has_morph_target = self.get_shape_keys(bobject.data)
if has_morph_target:
# Shape keys UV are exported separately, so reduce UV count by 1
num_uv_layers -= 1
morph_uv_index = self.get_morph_uv_index(bobject.data)
has_tex = (self.get_export_uvs(bobject.data) and num_uv_layers > 0) or is_baked
has_tex1 = has_tex and num_uv_layers > 1
has_tang = self.has_tangents(bobject.data)
pdata = np.empty(num_verts * 4, dtype='<f4') # p.xyz, n.z
ndata = np.empty(num_verts * 2, dtype='<f4') # n.xy
if has_tex:
t0map = 0 # Get active uvmap
t0data = np.empty(num_verts * 2, dtype='<f4')
if has_tex or has_morph_target:
uv_layers = exportMesh.uv_layers
if uv_layers is not None:
if 'UVMap_baked' in uv_layers:
for i in range(0, len(uv_layers)):
if uv_layers[i].name == 'UVMap_baked':
t0map = i
break
else:
for i in range(0, len(uv_layers)):
if uv_layers[i].active_render:
t0map = i
break
if has_tex1:
t1map = 1 if t0map == 0 else 0
t1data = np.empty(num_verts * 2, dtype='<f4')
# Scale for packed coords
maxdim = 1.0
lay0 = uv_layers[t0map]
for v in lay0.data:
if abs(v.uv[0]) > maxdim:
maxdim = abs(v.uv[0])
if abs(v.uv[1]) > maxdim:
maxdim = abs(v.uv[1])
if has_tex1:
lay1 = uv_layers[t1map]
for v in lay1.data:
if has_tex:
t0map = 0 # Get active uvmap
t0data = np.empty(num_verts * 2, dtype='<f4')
if uv_layers is not None:
if 'UVMap_baked' in uv_layers:
for i in range(0, len(uv_layers)):
if uv_layers[i].name == 'UVMap_baked':
t0map = i
break
else:
for i in range(0, len(uv_layers)):
if uv_layers[i].active_render and uv_layers[i].name != 'UVMap_shape_key':
t0map = i
break
if has_tex1:
for i in range(0, len(uv_layers)):
# Not UVMap 0
if i != t0map:
# Not Shape Key UVMap
if has_morph_target and uv_layers[i].name == 'UVMap_shape_key':
continue
# Neither UVMap 0 Nor Shape Key Map
t1map = i
t1data = np.empty(num_verts * 2, dtype='<f4')
# Scale for packed coords
lay0 = uv_layers[t0map]
for v in lay0.data:
if abs(v.uv[0]) > maxdim:
maxdim = abs(v.uv[0])
if abs(v.uv[1]) > maxdim:
maxdim = abs(v.uv[1])
if has_tex1:
lay1 = uv_layers[t1map]
for v in lay1.data:
if abs(v.uv[0]) > maxdim:
maxdim = abs(v.uv[0])
if abs(v.uv[1]) > maxdim:
maxdim = abs(v.uv[1])
if has_morph_target:
morph_data = np.empty(num_verts * 2, dtype='<f4')
lay2 = uv_layers[morph_uv_index]
for v in lay2.data:
if abs(v.uv[0]) > maxdim:
maxdim = abs(v.uv[0])
if abs(v.uv[1]) > maxdim:
@ -1220,6 +1431,8 @@ Make sure the mesh only has tris/quads.""")
lay0 = exportMesh.uv_layers[t0map]
if has_tex1:
lay1 = exportMesh.uv_layers[t1map]
if has_morph_target:
lay2 = exportMesh.uv_layers[morph_uv_index]
if has_col:
vcol0 = exportMesh.vertex_colors[0].data
@ -1250,6 +1463,10 @@ Make sure the mesh only has tris/quads.""")
tangdata[i3 ] = tang[0]
tangdata[i3 + 1] = tang[1]
tangdata[i3 + 2] = tang[2]
if has_morph_target:
uv = lay2.data[loop.index].uv
morph_data[i2 ] = uv[0]
morph_data[i2 + 1] = 1.0 - uv[1]
if has_col:
col = vcol0[loop.index].color
i3 = i * 3
@ -1310,6 +1527,9 @@ Make sure the mesh only has tris/quads.""")
if has_tex1:
t1data *= invscale_tex
t1data = np.array(t1data, dtype='<i2')
if has_morph_target:
morph_data *= invscale_tex
morph_data = np.array(morph_data, dtype='<i2')
if has_col:
cdata *= 32767
cdata = np.array(cdata, dtype='<i2')
@ -1325,6 +1545,8 @@ Make sure the mesh only has tris/quads.""")
o['vertex_arrays'].append({ 'attrib': 'tex', 'values': t0data, 'data': 'short2norm' })
if has_tex1:
o['vertex_arrays'].append({ 'attrib': 'tex1', 'values': t1data, 'data': 'short2norm' })
if has_morph_target:
o['vertex_arrays'].append({ 'attrib': 'morph', 'values': morph_data, 'data': 'short2norm' })
if has_col:
o['vertex_arrays'].append({ 'attrib': 'col', 'values': cdata, 'data': 'short4norm', 'padding': 1 })
if has_tang:
@ -1410,52 +1632,20 @@ Make sure the mesh only has tris/quads.""")
struct_flag = False
# Save the morph state if necessary
active_shape_key_index = bobject.active_shape_key_index
show_only_shape_key = bobject.show_only_shape_key
current_morph_value = []
active_shape_key_index = 0
show_only_shape_key = False
current_morph_value = 0
shape_keys = ArmoryExporter.get_shape_keys(mesh)
if shape_keys:
# Save the morph state
active_shape_key_index = bobject.active_shape_key_index
show_only_shape_key = bobject.show_only_shape_key
current_morph_value = bobject.active_shape_key.value
# Reset morph state to base for mesh export
bobject.active_shape_key_index = 0
bobject.show_only_shape_key = True
base_index = 0
relative = shape_keys.use_relative
if relative:
morph_count = 0
base_name = shape_keys.reference_key.name
for block in shape_keys.key_blocks:
if block.name == base_name:
base_index = morph_count
break
morph_count += 1
morph_count = 0
for block in shape_keys.key_blocks:
current_morph_value.append(block.value)
block.value = 0.0
if block.name != "":
# self.IndentWrite(B"Morph (index = ", 0, struct_flag)
# self.WriteInt(morph_count)
# if (relative) and (morph_count != base_index):
# self.Write(B", base = ")
# self.WriteInt(base_index)
# self.Write(B")\n")
# self.IndentWrite(B"{\n")
# self.IndentWrite(B"Name {string {\"", 1)
# self.Write(bytes(block.name, "UTF-8"))
# self.Write(B"\"}}\n")
# self.IndentWrite(B"}\n")
# TODO
struct_flag = True
morph_count += 1
shape_keys.key_blocks[0].value = 1.0
mesh.update()
self.depsgraph.update()
armature = bobject.find_armature()
apply_modifiers = not armature
@ -1463,6 +1653,14 @@ Make sure the mesh only has tris/quads.""")
bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject
export_mesh = bobject_eval.to_mesh()
# Export shape keys here
if shape_keys:
self.export_shape_keys(bobject, export_mesh, out_mesh)
# Update dependancy after new UV layer was added
self.depsgraph.update()
bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject
export_mesh = bobject_eval.to_mesh()
if export_mesh is None:
log.warn(oid + ' was not exported')
return
@ -1483,13 +1681,12 @@ Make sure the mesh only has tris/quads.""")
if armature:
self.export_skin(bobject, armature, export_mesh, out_mesh)
# Restore the morph state
# Restore the morph state after mesh export
if shape_keys:
bobject.active_shape_key_index = active_shape_key_index
bobject.show_only_shape_key = show_only_shape_key
for m in range(len(current_morph_value)):
shape_keys.key_blocks[m].value = current_morph_value[m]
bobject.active_shape_key.value = current_morph_value
self.depsgraph.update()
mesh.update()

View File

@ -0,0 +1,16 @@
from arm.logicnode.arm_nodes import *
class SetObjectShapeKeyNode(ArmLogicTreeNode):
"""Sets shape key value of the object"""
bl_idname = 'LNSetObjectShapeKeyNode'
bl_label = 'Set Object Shape Key'
arm_section = 'props'
arm_version = 1
def arm_init(self, context):
self.add_input('ArmNodeSocketAction', 'In')
self.add_input('ArmNodeSocketObject', 'Object')
self.add_input('ArmStringSocket', 'Shape Key')
self.add_input('ArmFloatSocket', 'Value')
self.add_output('ArmNodeSocketAction', 'Out')

View File

@ -900,6 +900,10 @@ def clean():
shutil.rmtree('Sources/' + pkg_dir, onerror=remove_readonly)
if os.path.exists('Sources') and os.listdir('Sources') == []:
shutil.rmtree('Sources/', onerror=remove_readonly)
# Remove Shape key Textures
if os.path.exists('MorphTargets/'):
shutil.rmtree('MorphTargets/', onerror=remove_readonly)
# To recache signatures for batched materials
for mat in bpy.data.materials:

View File

@ -6,6 +6,7 @@ import arm.material.make_skin as make_skin
import arm.material.make_particle as make_particle
import arm.material.make_inst as make_inst
import arm.material.make_tess as make_tess
import arm.material.make_morph_target as make_morph_target
from arm.material.shader import Shader, ShaderContext
import arm.utils
@ -16,6 +17,7 @@ if arm.is_reload(__name__):
make_particle = arm.reload_module(make_particle)
make_inst = arm.reload_module(make_inst)
make_tess = arm.reload_module(make_tess)
make_morph_target = arm.reload_module(make_morph_target)
arm.material.shader = arm.reload_module(arm.material.shader)
from arm.material.shader import Shader, ShaderContext
arm.utils = arm.reload_module(arm.utils)
@ -51,13 +53,18 @@ def write_vertpos(vert):
def write_norpos(con_mesh: ShaderContext, vert: Shader, declare=False, write_nor=True):
is_bone = con_mesh.is_elem('bone')
is_morph = con_mesh.is_elem('morph')
if is_morph:
make_morph_target.morph_pos(vert)
if is_bone:
make_skin.skin_pos(vert)
if write_nor:
prep = 'vec3 ' if declare else ''
if is_morph:
make_morph_target.morph_nor(vert, is_bone, prep)
if is_bone:
make_skin.skin_nor(vert, prep)
else:
make_skin.skin_nor(vert, is_morph, prep)
if not is_morph and not is_bone:
vert.write_attrib(prep + 'wnormal = normalize(N * vec3(nor.xy, pos.w));')
if con_mesh.is_elem('ipos'):
make_inst.inst_pos(con_mesh, vert)

View File

@ -8,6 +8,7 @@ import arm.material.make_inst as make_inst
import arm.material.make_tess as make_tess
import arm.material.make_particle as make_particle
import arm.material.make_finalize as make_finalize
import arm.material.make_morph_target as make_morph_target
import arm.assets as assets
import arm.utils
@ -50,6 +51,9 @@ def make(context_id, rpasses, shadowmap=False):
if parse_opacity:
frag.write('float opacity;')
if(con_depth).is_elem('morph'):
make_morph_target.morph_pos(vert)
if con_depth.is_elem('bone'):
make_skin.skin_pos(vert)

View File

@ -0,0 +1,28 @@
import arm.utils
if arm.is_reload(__name__):
arm.utils = arm.reload_module(arm.utils)
else:
arm.enable_reload(__name__)
def morph_pos(vert):
rpdat = arm.utils.get_rp()
vert.add_include('compiled.inc')
vert.add_include('std/morph_target.glsl')
vert.add_uniform('sampler2D morphDataPos', link='_morphDataPos', included=True)
vert.add_uniform('sampler2D morphDataNor', link='_morphDataNor', included=True)
vert.add_uniform('vec4 morphWeights[8]', link='_morphWeights', included=True)
vert.add_uniform('vec2 morphScaleOffset', link='_morphScaleOffset', included=True)
vert.add_uniform('vec2 morphDataDim', link='_morphDataDim', included=True)
vert.add_uniform('float texUnpack', link='_texUnpack')
vert.add_uniform('float posUnpack', link='_posUnpack')
vert.write_attrib('vec2 texCoordMorph = morph * texUnpack;')
vert.write_attrib('spos.xyz *= posUnpack;')
vert.write_attrib('getMorphedVertex(texCoordMorph, spos.xyz);')
vert.write_attrib('spos.xyz /= posUnpack;')
def morph_nor(vert, is_bone, prep):
vert.write_attrib('vec3 morphNor;')
vert.write_attrib('getMorphedNormal(texCoordMorph, vec3(nor.xy, pos.w), morphNor);')
if not is_bone:
vert.write_attrib(prep + 'wnormal = normalize(N * morphNor);')

View File

@ -187,6 +187,9 @@ def make_instancing_and_skinning(mat: Material, mat_users: Dict[Material, List[O
for bo in mat_users[mat]:
if mat.arm_custom_material == '':
# Morph Targets
if arm.utils.export_morph_targets(bo):
global_elems.append({'name': 'morph', 'data': 'short2norm'})
# GPU Skinning
if arm.utils.export_bone_data(bo):
global_elems.append({'name': 'bone', 'data': 'short4norm'})

View File

@ -22,6 +22,9 @@ def skin_pos(vert):
vert.write_attrib('spos.xyz /= posUnpack;')
def skin_nor(vert, prep):
def skin_nor(vert, is_morph, prep):
rpdat = arm.utils.get_rp()
vert.write_attrib(prep + 'wnormal = normalize(N * (vec3(nor.xy, pos.w) + 2.0 * cross(skinA.xyz, cross(skinA.xyz, vec3(nor.xy, pos.w)) + skinA.w * vec3(nor.xy, pos.w))));')
if(is_morph):
vert.write_attrib(prep + 'wnormal = normalize(N * (morphNor + 2.0 * cross(skinA.xyz, cross(skinA.xyz, morphNor) + skinA.w * morphNor)));')
else:
vert.write_attrib(prep + 'wnormal = normalize(N * (vec3(nor.xy, pos.w) + 2.0 * cross(skinA.xyz, cross(skinA.xyz, vec3(nor.xy, pos.w)) + skinA.w * vec3(nor.xy, pos.w))));')

View File

@ -92,7 +92,7 @@ class ShaderContext:
def sort_vs(self):
vs = []
ar = ['pos', 'nor', 'tex', 'tex1', 'col', 'tang', 'bone', 'weight', 'ipos', 'irot', 'iscl']
ar = ['pos', 'nor', 'tex', 'tex1', 'morph', 'col', 'tang', 'bone', 'weight', 'ipos', 'irot', 'iscl']
for ename in ar:
elem = self.get_elem(ename)
if elem != None:

View File

@ -567,6 +567,10 @@ class ArmRPListItem(bpy.types.PropertyGroup):
name='Skinning', description='Enable skinning', default='On', update=assets.invalidate_shader_cache)
arm_skin_max_bones_auto: BoolProperty(name="Auto Bones", description="Calculate amount of maximum bones based on armatures", default=True, update=assets.invalidate_compiled_data)
arm_skin_max_bones: IntProperty(name="Max Bones", default=50, min=1, max=3000, update=assets.invalidate_shader_cache)
arm_morph_target: EnumProperty(
items=[('On', 'On', 'On'),
('Off', 'Off', 'Off')],
name='Shape key', description='Enable shape keys', default='On', update=assets.invalidate_shader_cache)
arm_particles: EnumProperty(
items=[('On', 'On', 'On'),
('Off', 'Off', 'Off')],

View File

@ -1343,6 +1343,12 @@ class ARM_PT_RenderPathRendererPanel(bpy.types.Panel):
row.enabled = not rpdat.arm_skin_max_bones_auto
row.prop(rpdat, 'arm_skin_max_bones')
layout.separator(factor=0.1)
col = layout.column()
col.prop(rpdat, 'arm_morph_target')
col = col.column()
col.enabled = rpdat.arm_morph_target == 'On'
layout.separator(factor=0.1)
col = layout.column()
col.prop(rpdat, "rp_hdr")

View File

@ -770,6 +770,28 @@ def export_bone_data(bobject: bpy.types.Object) -> bool:
"""Returns whether the bone data of the given object should be exported."""
return bobject.find_armature() and is_bone_animation_enabled(bobject) and get_rp().arm_skin == 'On'
def export_morph_targets(bobject: bpy.types.Object) -> bool:
if get_rp().arm_morph_target != 'On':
return False
if not hasattr(bobject.data, 'shape_keys'):
return False
shape_keys = bobject.data.shape_keys
if not shape_keys:
return False
if len(shape_keys.key_blocks) < 2:
return False
for shape_key in shape_keys.key_blocks[1:]:
if(not shape_key.mute):
return True
return False
def export_vcols(bobject: bpy.types.Object) -> bool:
for material in bobject.data.materials:
if material is not None and material.export_vcols:
return True
return False
def open_editor(hx_path=None):
ide_bin = get_ide_bin()

View File

@ -87,6 +87,12 @@ project.addSources('Sources');
for file in glob.glob("Bundled/**", recursive=True):
if os.path.isfile(file):
assets.add(file)
# Auto-add shape key textures if exists
if os.path.exists('MorphTargets'):
for file in glob.glob("MorphTargets/**", recursive=True):
if os.path.isfile(file):
assets.add(file)
# Add project shaders
if os.path.exists('Shaders'):
@ -297,6 +303,9 @@ project.addSources('Sources');
rpdat = arm.utils.get_rp()
if rpdat.arm_skin != 'Off':
assets.add_khafile_def('arm_skin')
if rpdat.arm_morph_target != 'Off':
assets.add_khafile_def('arm_morph_target')
if rpdat.arm_particles != 'Off':
assets.add_khafile_def('arm_particles')