Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ext mesh gpu instancing #1984

Merged
merged 5 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions addons/io_scene_gltf2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,14 @@ def __init__(self):
default=False
)

export_gpu_instances: BoolProperty(
name='GPU Instances',
description='Export using EXT_mesh_gpu_instancing.'
'Limited to children of a same Empty. '
'multiple Materials might be omitted',
default=False
)

# This parameter is only here for backward compatibility, as this option is removed in 3.6
# This option does nothing, and is not displayed in UI
# What you are looking for is probably "export_animation_mode"
Expand Down Expand Up @@ -848,12 +856,14 @@ def execute(self, context):

export_settings['gltf_lights'] = self.export_lights
export_settings['gltf_lighting_mode'] = self.export_import_convert_lighting_mode
export_settings['gltf_gpu_instances'] = self.export_gpu_instances

export_settings['gltf_try_sparse_sk'] = self.export_try_sparse_sk
export_settings['gltf_try_omit_sparse_sk'] = self.export_try_omit_sparse_sk
if not self.export_try_sparse_sk:
export_settings['gltf_try_omit_sparse_sk'] = False


export_settings['gltf_binary'] = bytearray()
export_settings['gltf_binaryfilename'] = (
path_to_uri(os.path.splitext(os.path.basename(self.filepath))[0] + '.bin')
Expand Down Expand Up @@ -1002,6 +1012,28 @@ def poll(cls, context):
def draw(self, context):
pass

class GLTF_PT_export_data_scene(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Scene Graph"
bl_parent_id = "GLTF_PT_export_data"
bl_options = {'DEFAULT_CLOSED'}

@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENE_OT_gltf"

def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.

sfile = context.space_data
operator = sfile.active_operator
layout.prop(operator, 'export_gpu_instances')

class GLTF_PT_export_data_mesh(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
Expand Down Expand Up @@ -1791,6 +1823,7 @@ def menu_func_import(self, context):
GLTF_PT_export_include,
GLTF_PT_export_transform,
GLTF_PT_export_data,
GLTF_PT_export_data_scene,
GLTF_PT_export_data_mesh,
GLTF_PT_export_data_material,
GLTF_PT_export_data_original_pbr,
Expand Down
2 changes: 1 addition & 1 deletion addons/io_scene_gltf2/blender/exp/gltf2_blender_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def __gather_gltf(exporter, export_settings):
export_user_extensions('gather_gltf_hook', export_settings, active_scene_idx, scenes, animations)

for idx, scene in enumerate(scenes):
exporter.add_scene(scene, idx==active_scene_idx)
exporter.add_scene(scene, idx==active_scene_idx, export_settings=export_settings)
for animation in animations:
exporter.add_animation(animation)
exporter.traverse_unused_skins(unused_skins)
Expand Down
192 changes: 179 additions & 13 deletions addons/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
from ... import get_version_string
from ...io.com import gltf2_io, gltf2_io_extensions
from ...io.com.gltf2_io_path import path_to_uri, uri_to_path
from ...io.com.gltf2_io_constants import ComponentType, DataType
from ...io.exp import gltf2_io_binary_data, gltf2_io_buffer, gltf2_io_image_data
from ...io.exp.gltf2_io_user_extensions import export_user_extensions
from .gltf2_blender_gather_accessors import gather_accessor


class GlTF2Exporter:
Expand Down Expand Up @@ -164,7 +166,119 @@ def finalize_images(self):
with open(dst_path, 'wb') as f:
f.write(image.data)

def add_scene(self, scene: gltf2_io.Scene, active: bool = False):

def manage_gpu_instancing(self, node, also_mesh=False):
instances = {}
for child_idx in node.children:
child = self.__gltf.nodes[child_idx]
if child.children:
continue
if child.mesh is not None and child.mesh not in instances.keys():
instances[child.mesh] = []
if child.mesh is not None:
instances[child.mesh].append(child_idx)

# For now, manage instances only if there are all children of same object
# And this instances don't have any children
instances = {k:v for k, v in instances.items() if len(v) > 1}

holders = []
if len(instances.keys()) == 1 and also_mesh is False:
# There is only 1 set of instances. So using the parent as instance holder
holder = node
holders = [node]
elif len(instances.keys()) > 1 or (len(instances.keys()) == 1 and also_mesh is True):
for h in range(len(instances.keys())):
# Create a new node
n = gltf2_io.Node(
camera=None,
children=[],
extensions=None,
extras=None,
matrix=None,
mesh=None,
name=node.name + "." + str(h),
rotation=None,
scale=None,
skin=None,
translation=None,
weights=None,
)
n = self.__traverse_property(n)
idx = self.__to_reference(n)

# Add it to original empty
node.children.append(idx)
holders.append(self.__gltf.nodes[idx])

for idx, inst_key in enumerate(instances.keys()):
insts = instances[inst_key]
holder = holders[idx]

# Let's retrieve TRS of instances
translation = []
rotation = []
scale = []
for inst_node_idx in insts:
inst_node = self.__gltf.nodes[inst_node_idx]
t = inst_node.translation if inst_node.translation is not None else [0,0,0]
r = inst_node.rotation if inst_node.rotation is not None else [0,0,0,1]
s = inst_node.scale if inst_node.scale is not None else [1,1,1]
for i in t:
translation.append(i)
for i in r:
rotation.append(i)
for i in s:
scale.append(i)

# Create Accessors for the extension
ext = {}
ext['attributes'] = {}
ext['attributes']['TRANSLATION'] = gather_accessor(
gltf2_io_binary_data.BinaryData.from_list(translation, ComponentType.Float),
ComponentType.Float,
len(translation) // 3,
None,
None,
DataType.Vec3,
None
)
ext['attributes']['ROTATION'] = gather_accessor(
gltf2_io_binary_data.BinaryData.from_list(rotation, ComponentType.Float),
ComponentType.Float,
len(rotation) // 4,
None,
None,
DataType.Vec4,
None
)
ext['attributes']['SCALE'] = gather_accessor(
gltf2_io_binary_data.BinaryData.from_list(scale, ComponentType.Float),
ComponentType.Float,
len(scale) // 3,
None,
None,
DataType.Vec3,
None
)

# Add extension to the Node, and traverse it
if not holder.extensions:
holder.extensions = {}
holder.extensions["EXT_mesh_gpu_instancing"] = gltf2_io_extensions.Extension('EXT_mesh_gpu_instancing', ext, False)
holder.mesh = inst_key
self.__traverse(holder.extensions)

# Remove children from original Empty
new_children = []
for child_idx in node.children:
if child_idx not in insts:
new_children.append(child_idx)
node.children = new_children

self.nodes_idx_to_remove.extend(insts)

def add_scene(self, scene: gltf2_io.Scene, active: bool = False, export_settings=None):
"""
Add a scene to the glTF.

Expand All @@ -176,12 +290,63 @@ def add_scene(self, scene: gltf2_io.Scene, active: bool = False):
if self.__finalized:
raise RuntimeError("Tried to add scene to finalized glTF file")

# for node in scene.nodes:
# self.__traverse(node)
scene_num = self.__traverse(scene)
if active:
self.__gltf.scene = scene_num

if export_settings['gltf_gpu_instances'] is True:
# Modify the scene data in case of EXT_mesh_gpu_instancing export

self.nodes_idx_to_remove = []
for node_idx in self.__gltf.scenes[scene_num].nodes:
node = self.__gltf.nodes[node_idx]
if node.mesh is None:
self.manage_gpu_instancing(node)
else:
self.manage_gpu_instancing(node, also_mesh=True)
for child_idx in node.children:
child = self.__gltf.nodes[child_idx]
self.manage_gpu_instancing(child, also_mesh=child.mesh is not None)

# Slides other nodes index

self.nodes_idx_to_remove.sort()
for node_idx in self.__gltf.scenes[scene_num].nodes:
self.recursive_slide_node_idx(node_idx)

new_node_list = []
for node_idx in self.__gltf.scenes[scene_num].nodes:
len_ = len([i for i in self.nodes_idx_to_remove if i < node_idx])
new_node_list.append(node_idx - len_)
self.__gltf.scenes[scene_num].nodes = new_node_list

for skin in self.__gltf.skins:
new_joint_list = []
for node_idx in skin.joints:
len_ = len([i for i in self.nodes_idx_to_remove if i < node_idx])
new_joint_list.append(node_idx - len_)
skin.joints = new_joint_list
if skin.skeleton is not None:
len_ = len([i for i in self.nodes_idx_to_remove if i < skin.skeleton])
skin.skeleton = skin.skeleton - len_

# And now really remove nodes
self.__gltf.nodes = [node for idx, node in enumerate(self.__gltf.nodes) if idx not in self.nodes_idx_to_remove]

def recursive_slide_node_idx(self, node_idx):
node = self.__gltf.nodes[node_idx]

new_node_children = []
for child_idx in node.children:
len_ = len([i for i in self.nodes_idx_to_remove if i < child_idx])
new_node_children.append(child_idx - len_)


for child_idx in node.children:
self.recursive_slide_node_idx(child_idx)

node.children = new_node_children

def traverse_unused_skins(self, skins):
for s in skins:
self.__traverse(s)
Expand Down Expand Up @@ -264,14 +429,7 @@ def __get_key_path(cls, d: dict, keypath: List[str], default):
def traverse_extensions(self):
self.__traverse(self.__gltf.extensions)

def __traverse(self, node):
"""
Recursively traverse a scene graph consisting of gltf compatible elements.

The tree is traversed downwards until a primitive is reached. Then any ChildOfRoot property
is stored in the according list in the glTF and replaced with a index reference in the upper level.
"""
def __traverse_property(node):
def __traverse_property(self, node):
for member_name in [a for a in dir(node) if not a.startswith('__') and not callable(getattr(node, a))]:
new_value = self.__traverse(getattr(node, member_name))
setattr(node, member_name, new_value) # usually this is the same as before
Expand All @@ -283,9 +441,17 @@ def __traverse_property(node):
# self.__append_unique_and_get_index(self.__gltf.extensions_required, extension_name)
return node


def __traverse(self, node):
"""
Recursively traverse a scene graph consisting of gltf compatible elements.

The tree is traversed downwards until a primitive is reached. Then any ChildOfRoot property
is stored in the according list in the glTF and replaced with a index reference in the upper level.
"""
# traverse nodes of a child of root property type and add them to the glTF root
if type(node) in self.__childOfRootPropertyTypeLookup:
node = __traverse_property(node)
node = self.__traverse_property(node)
idx = self.__to_reference(node)
# child of root properties are only present at root level --> replace with index in upper level
return idx
Expand All @@ -303,7 +469,7 @@ def __traverse_property(node):

# traverse into any other property
if type(node) in self.__propertyTypeLookup:
return __traverse_property(node)
return self.__traverse_property(node)

# binary data needs to be moved to a buffer and referenced with a buffer view
if isinstance(node, gltf2_io_binary_data.BinaryData):
Expand Down
17 changes: 15 additions & 2 deletions addons/io_scene_gltf2/blender/imp/gltf2_blender_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def create_vnode(gltf, vnode_id):
if bpy.app.debug_value == 101:
gltf.log.critical("Node %d of %d (id %s)", gltf.display_current_node, len(gltf.vnodes), vnode_id)

if vnode.type == VNode.Object:
if vnode.type in [VNode.Object, VNode.Inst]:
gltf_node = gltf.data.nodes[vnode_id] if isinstance(vnode_id, int) else None
import_user_extensions('gather_import_node_before_hook', gltf, vnode, gltf_node)
obj = BlenderNode.create_object(gltf, vnode_id)
Expand All @@ -62,6 +62,9 @@ def create_object(gltf, vnode_id):
if vnode.mesh_node_idx is not None:
obj = BlenderNode.create_mesh_object(gltf, vnode)

elif vnode.type == VNode.Inst and vnode.mesh_idx is not None:
obj = BlenderNode.create_mesh_object(gltf, vnode)

elif vnode.camera_node_idx is not None:
pynode = gltf.data.nodes[vnode.camera_node_idx]
cam = BlenderCamera.create(gltf, vnode, pynode.camera)
Expand Down Expand Up @@ -236,7 +239,17 @@ def visit(id): # Depth-first walk

@staticmethod
def create_mesh_object(gltf, vnode):
pynode = gltf.data.nodes[vnode.mesh_node_idx]
if vnode.type != VNode.Inst:
# Regular case
pynode = gltf.data.nodes[vnode.mesh_node_idx]
else:
class DummyPyNode:
pass
pynode = DummyPyNode()
pynode.mesh = vnode.mesh_idx
pynode.skin = None
pynode.weights = None

if not (0 <= pynode.mesh < len(gltf.data.meshes)):
# Avoid traceback for invalid gltf file: invalid reference to meshes array
# So return an empty blender object)
Expand Down
Loading