Mesh: Update addons for auto smooth removal #104609
@ -6,7 +6,7 @@ bl_info = {
|
|||||||
"name": "Grease Pencil Tools",
|
"name": "Grease Pencil Tools",
|
||||||
"description": "Extra tools for Grease Pencil",
|
"description": "Extra tools for Grease Pencil",
|
||||||
"author": "Samuel Bernou, Antonio Vazquez, Daniel Martinez Lara, Matias Mendiola",
|
"author": "Samuel Bernou, Antonio Vazquez, Daniel Martinez Lara, Matias Mendiola",
|
||||||
"version": (1, 8, 1),
|
"version": (1, 8, 2),
|
||||||
"blender": (3, 0, 0),
|
"blender": (3, 0, 0),
|
||||||
"location": "Sidebar > Grease Pencil > Grease Pencil Tools",
|
"location": "Sidebar > Grease Pencil > Grease Pencil Tools",
|
||||||
"warning": "",
|
"warning": "",
|
||||||
|
@ -49,10 +49,10 @@ def get_reduced_area_coord(context):
|
|||||||
|
|
||||||
## minus tool leftbar + sidebar right
|
## minus tool leftbar + sidebar right
|
||||||
regs = context.area.regions
|
regs = context.area.regions
|
||||||
toolbar = regs[2]
|
toolbar = next((r for r in regs if r.type == 'TOOLS'), None)
|
||||||
sidebar = regs[3]
|
sidebar = next((r for r in regs if r.type == 'UI'), None)
|
||||||
header = regs[0]
|
header = next((r for r in regs if r.type == 'HEADER'), None)
|
||||||
tool_header = regs[1]
|
tool_header = next((r for r in regs if r.type == 'TOOL_HEADER'), None)
|
||||||
up_margin = down_margin = 0
|
up_margin = down_margin = 0
|
||||||
if tool_header.alignment == 'TOP':
|
if tool_header.alignment == 'TOP':
|
||||||
up_margin += tool_header.height
|
up_margin += tool_header.height
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Import Images as Planes",
|
"name": "Import Images as Planes",
|
||||||
"author": "Florian Meyer (tstscr), mont29, matali, Ted Schundler (SpkyElctrc), mrbimax",
|
"author": "Florian Meyer (tstscr), mont29, matali, Ted Schundler (SpkyElctrc), mrbimax",
|
||||||
"version": (3, 5, 0),
|
"version": (3, 5, 1),
|
||||||
"blender": (2, 91, 0),
|
"blender": (4, 0, 0),
|
||||||
"location": "File > Import > Images as Planes or Add > Image > Images as Planes",
|
"location": "File > Import > Images as Planes or Add > Image > Images as Planes",
|
||||||
"description": "Imports images and creates planes with the appropriate aspect ratio. "
|
"description": "Imports images and creates planes with the appropriate aspect ratio. "
|
||||||
"The images are mapped to the planes.",
|
"The images are mapped to the planes.",
|
||||||
@ -25,7 +25,10 @@ from math import pi
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Operator
|
from bpy.types import Operator
|
||||||
from bpy.app.translations import pgettext_tip as tip_
|
from bpy.app.translations import (
|
||||||
|
pgettext_tip as tip_,
|
||||||
|
contexts as i18n_contexts
|
||||||
|
)
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
|
|
||||||
from bpy.props import (
|
from bpy.props import (
|
||||||
@ -151,6 +154,9 @@ def load_images(filenames, directory, force_reload=False, frame_start=1, find_se
|
|||||||
file_iter = zip(filenames, repeat(1), repeat(1))
|
file_iter = zip(filenames, repeat(1), repeat(1))
|
||||||
|
|
||||||
for filename, offset, frames in file_iter:
|
for filename, offset, frames in file_iter:
|
||||||
|
if not os.path.isfile(bpy.path.abspath(os.path.join(directory, filename))):
|
||||||
|
continue
|
||||||
|
|
||||||
image = load_image(filename, directory, check_existing=True, force_reload=force_reload)
|
image = load_image(filename, directory, check_existing=True, force_reload=force_reload)
|
||||||
|
|
||||||
# Size is unavailable for sequences, so we grab it early
|
# Size is unavailable for sequences, so we grab it early
|
||||||
@ -320,8 +326,8 @@ def get_shadeless_node(dest_node_tree):
|
|||||||
output_node = node_tree.nodes.new('NodeGroupOutput')
|
output_node = node_tree.nodes.new('NodeGroupOutput')
|
||||||
input_node = node_tree.nodes.new('NodeGroupInput')
|
input_node = node_tree.nodes.new('NodeGroupInput')
|
||||||
|
|
||||||
node_tree.outputs.new('NodeSocketShader', 'Shader')
|
node_tree.interface.new_socket('Shader', in_out='OUTPUT', socket_type='NodeSocketShader')
|
||||||
node_tree.inputs.new('NodeSocketColor', 'Color')
|
node_tree.interface.new_socket('Color', in_out='INPUT', socket_type='NodeSocketColor')
|
||||||
|
|
||||||
# This could be faster as a transparent shader, but then no ambient occlusion
|
# This could be faster as a transparent shader, but then no ambient occlusion
|
||||||
diffuse_shader = node_tree.nodes.new('ShaderNodeBsdfDiffuse')
|
diffuse_shader = node_tree.nodes.new('ShaderNodeBsdfDiffuse')
|
||||||
@ -731,7 +737,9 @@ class IMPORT_IMAGE_OT_to_plane(Operator, AddObjectHelper):
|
|||||||
('HASHED', "Hashed","Use noise to dither the binary visibility (works well with multi-samples)"),
|
('HASHED', "Hashed","Use noise to dither the binary visibility (works well with multi-samples)"),
|
||||||
('OPAQUE', "Opaque","Render surface without transparency"),
|
('OPAQUE', "Opaque","Render surface without transparency"),
|
||||||
)
|
)
|
||||||
blend_method: EnumProperty(name="Blend Mode", items=BLEND_METHODS, default='BLEND', description="Blend Mode for Transparent Faces")
|
blend_method: EnumProperty(
|
||||||
|
name="Blend Mode", items=BLEND_METHODS, default='BLEND',
|
||||||
|
description="Blend Mode for Transparent Faces", translation_context=i18n_contexts.id_material)
|
||||||
|
|
||||||
SHADOW_METHODS = (
|
SHADOW_METHODS = (
|
||||||
('CLIP', "Clip","Use the alpha threshold to clip the visibility (binary visibility)"),
|
('CLIP', "Clip","Use the alpha threshold to clip the visibility (binary visibility)"),
|
||||||
@ -739,7 +747,9 @@ class IMPORT_IMAGE_OT_to_plane(Operator, AddObjectHelper):
|
|||||||
('OPAQUE',"Opaque","Material will cast shadows without transparency"),
|
('OPAQUE',"Opaque","Material will cast shadows without transparency"),
|
||||||
('NONE',"None","Material will cast no shadow"),
|
('NONE',"None","Material will cast no shadow"),
|
||||||
)
|
)
|
||||||
shadow_method: EnumProperty(name="Shadow Mode", items=SHADOW_METHODS, default='CLIP', description="Shadow mapping method")
|
shadow_method: EnumProperty(
|
||||||
|
name="Shadow Mode", items=SHADOW_METHODS, default='CLIP',
|
||||||
|
description="Shadow mapping method", translation_context=i18n_contexts.id_material)
|
||||||
|
|
||||||
use_backface_culling: BoolProperty(
|
use_backface_culling: BoolProperty(
|
||||||
name="Backface Culling", default=False,
|
name="Backface Culling", default=False,
|
||||||
@ -923,11 +933,11 @@ class IMPORT_IMAGE_OT_to_plane(Operator, AddObjectHelper):
|
|||||||
if context.active_object and context.active_object.mode != 'OBJECT':
|
if context.active_object and context.active_object.mode != 'OBJECT':
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
self.import_images(context)
|
ret_code = self.import_images(context)
|
||||||
|
|
||||||
context.preferences.edit.use_enter_edit_mode = editmode
|
context.preferences.edit.use_enter_edit_mode = editmode
|
||||||
|
|
||||||
return {'FINISHED'}
|
return ret_code
|
||||||
|
|
||||||
def import_images(self, context):
|
def import_images(self, context):
|
||||||
|
|
||||||
@ -939,6 +949,10 @@ class IMPORT_IMAGE_OT_to_plane(Operator, AddObjectHelper):
|
|||||||
find_sequences=self.image_sequence
|
find_sequences=self.image_sequence
|
||||||
))
|
))
|
||||||
|
|
||||||
|
if not images:
|
||||||
|
self.report({'WARNING'}, "Please select at least an image.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
# Create individual planes
|
# Create individual planes
|
||||||
planes = [self.single_image_spec_to_plane(context, img_spec) for img_spec in images]
|
planes = [self.single_image_spec_to_plane(context, img_spec) for img_spec in images]
|
||||||
|
|
||||||
@ -962,6 +976,7 @@ class IMPORT_IMAGE_OT_to_plane(Operator, AddObjectHelper):
|
|||||||
|
|
||||||
# all done!
|
# all done!
|
||||||
self.report({'INFO'}, tip_("Added {} Image Plane(s)").format(len(planes)))
|
self.report({'INFO'}, tip_("Added {} Image Plane(s)").format(len(planes)))
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
# operate on a single image
|
# operate on a single image
|
||||||
def single_image_spec_to_plane(self, context, img_spec):
|
def single_image_spec_to_plane(self, context, img_spec):
|
||||||
@ -1079,7 +1094,7 @@ class IMPORT_IMAGE_OT_to_plane(Operator, AddObjectHelper):
|
|||||||
if self.shader in {'PRINCIPLED', 'SHADELESS'}:
|
if self.shader in {'PRINCIPLED', 'SHADELESS'}:
|
||||||
node_tree.links.new(core_shader.inputs[0], tex_image.outputs['Color'])
|
node_tree.links.new(core_shader.inputs[0], tex_image.outputs['Color'])
|
||||||
elif self.shader == 'EMISSION':
|
elif self.shader == 'EMISSION':
|
||||||
node_tree.links.new(core_shader.inputs['Emission'], tex_image.outputs['Color'])
|
node_tree.links.new(core_shader.inputs['Emission Color'], tex_image.outputs['Color'])
|
||||||
|
|
||||||
if self.use_transparency:
|
if self.use_transparency:
|
||||||
if self.shader in {'PRINCIPLED', 'EMISSION'}:
|
if self.shader in {'PRINCIPLED', 'EMISSION'}:
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "UV Layout",
|
"name": "UV Layout",
|
||||||
"author": "Campbell Barton, Matt Ebb",
|
"author": "Campbell Barton, Matt Ebb",
|
||||||
"version": (1, 1, 6),
|
"version": (1, 2, 0),
|
||||||
"blender": (3, 0, 0),
|
"blender": (3, 0, 0),
|
||||||
"location": "UV Editor > UV > Export UV Layout",
|
"location": "UV Editor > UV > Export UV Layout",
|
||||||
"description": "Export the UV layout as a 2D graphic",
|
"description": "Export the UV layout as a 2D graphic",
|
||||||
@ -30,6 +30,8 @@ if "bpy" in locals():
|
|||||||
import os
|
import os
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
from bpy.app.translations import contexts as i18n_contexts
|
||||||
|
|
||||||
from bpy.props import (
|
from bpy.props import (
|
||||||
StringProperty,
|
StringProperty,
|
||||||
BoolProperty,
|
BoolProperty,
|
||||||
@ -54,10 +56,24 @@ class ExportUVLayout(bpy.types.Operator):
|
|||||||
description="Export all UVs in this mesh (not just visible ones)",
|
description="Export all UVs in this mesh (not just visible ones)",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
export_tiles: EnumProperty(
|
||||||
|
name="Export Tiles",
|
||||||
|
items=(
|
||||||
|
('NONE', "None",
|
||||||
|
"Export only UVs in the [0, 1] range"),
|
||||||
|
('UDIM', "UDIM",
|
||||||
|
"Export tiles in the UDIM numbering scheme: 1001 + u-tile + 10*v-tile"),
|
||||||
|
('UV', "UVTILE",
|
||||||
|
"Export tiles in the UVTILE numbering scheme: u(u-tile + 1)_v(v-tile + 1)"),
|
||||||
|
),
|
||||||
|
description="Choose whether to export only the [0, 1 range], or all UV tiles",
|
||||||
|
default='NONE',
|
||||||
|
)
|
||||||
modified: BoolProperty(
|
modified: BoolProperty(
|
||||||
name="Modified",
|
name="Modified",
|
||||||
description="Exports UVs from the modified mesh",
|
description="Exports UVs from the modified mesh",
|
||||||
default=False,
|
default=False,
|
||||||
|
translation_context=i18n_contexts.id_mesh,
|
||||||
)
|
)
|
||||||
mode: EnumProperty(
|
mode: EnumProperty(
|
||||||
items=(
|
items=(
|
||||||
@ -73,6 +89,7 @@ class ExportUVLayout(bpy.types.Operator):
|
|||||||
default='PNG',
|
default='PNG',
|
||||||
)
|
)
|
||||||
size: IntVectorProperty(
|
size: IntVectorProperty(
|
||||||
|
name="Size",
|
||||||
size=2,
|
size=2,
|
||||||
default=(1024, 1024),
|
default=(1024, 1024),
|
||||||
min=8, max=32768,
|
min=8, max=32768,
|
||||||
@ -123,9 +140,6 @@ class ExportUVLayout(bpy.types.Operator):
|
|||||||
if is_editmode:
|
if is_editmode:
|
||||||
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
||||||
|
|
||||||
filepath = self.filepath
|
|
||||||
filepath = bpy.path.ensure_ext(filepath, "." + self.mode.lower())
|
|
||||||
|
|
||||||
meshes = list(self.iter_meshes_to_export(context))
|
meshes = list(self.iter_meshes_to_export(context))
|
||||||
polygon_data = list(self.iter_polygon_data_to_draw(context, meshes))
|
polygon_data = list(self.iter_polygon_data_to_draw(context, meshes))
|
||||||
different_colors = set(color for _, color in polygon_data)
|
different_colors = set(color for _, color in polygon_data)
|
||||||
@ -135,8 +149,35 @@ class ExportUVLayout(bpy.types.Operator):
|
|||||||
obj_eval = obj.evaluated_get(depsgraph)
|
obj_eval = obj.evaluated_get(depsgraph)
|
||||||
obj_eval.to_mesh_clear()
|
obj_eval.to_mesh_clear()
|
||||||
|
|
||||||
|
tiles = self.tiles_to_export(polygon_data)
|
||||||
export = self.get_exporter()
|
export = self.get_exporter()
|
||||||
export(filepath, polygon_data, different_colors, self.size[0], self.size[1], self.opacity)
|
dirname, filename = os.path.split(self.filepath)
|
||||||
|
|
||||||
|
# Strip UDIM or UV numbering, and extension
|
||||||
|
import re
|
||||||
|
name_regex = r"^(.*?)"
|
||||||
|
udim_regex = r"(?:\.[0-9]{4})?"
|
||||||
|
uv_regex = r"(?:\.u[0-9]+_v[0-9]+)?"
|
||||||
|
ext_regex = r"(?:\.png|\.eps|\.svg)?$"
|
||||||
|
if self.export_tiles == 'NONE':
|
||||||
|
match = re.match(name_regex + ext_regex, filename)
|
||||||
|
elif self.export_tiles == 'UDIM':
|
||||||
|
match = re.match(name_regex + udim_regex + ext_regex, filename)
|
||||||
|
elif self.export_tiles == 'UV':
|
||||||
|
match = re.match(name_regex + uv_regex + ext_regex, filename)
|
||||||
|
if match:
|
||||||
|
filename = match.groups()[0]
|
||||||
|
|
||||||
|
for tile in sorted(tiles):
|
||||||
|
filepath = os.path.join(dirname, filename)
|
||||||
|
if self.export_tiles == 'UDIM':
|
||||||
|
filepath += f".{1001 + tile[0] + tile[1] * 10:04}"
|
||||||
|
elif self.export_tiles == 'UV':
|
||||||
|
filepath += f".u{tile[0] + 1}_v{tile[1] + 1}"
|
||||||
|
filepath = bpy.path.ensure_ext(filepath, "." + self.mode.lower())
|
||||||
|
|
||||||
|
export(filepath, tile, polygon_data, different_colors,
|
||||||
|
self.size[0], self.size[1], self.opacity)
|
||||||
|
|
||||||
if is_editmode:
|
if is_editmode:
|
||||||
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
|
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
|
||||||
@ -161,6 +202,30 @@ class ExportUVLayout(bpy.types.Operator):
|
|||||||
continue
|
continue
|
||||||
yield obj
|
yield obj
|
||||||
|
|
||||||
|
def tiles_to_export(self, polygon_data):
|
||||||
|
"""Get a set of tiles containing UVs.
|
||||||
|
This assumes there is no UV edge crossing an otherwise empty tile.
|
||||||
|
"""
|
||||||
|
if self.export_tiles == 'NONE':
|
||||||
|
return {(0, 0)}
|
||||||
|
|
||||||
|
from math import floor
|
||||||
|
tiles = set()
|
||||||
|
for poly in polygon_data:
|
||||||
|
for uv in poly[0]:
|
||||||
|
# Ignore UVs at corners - precisely touching the right or upper edge
|
||||||
|
# of a tile should not load its right/upper neighbor as well.
|
||||||
|
# From intern/cycles/scene/attribute.cpp
|
||||||
|
u, v = uv[0], uv[1]
|
||||||
|
x, y = floor(u), floor(v)
|
||||||
|
if x > 0 and u < x + 1e-6:
|
||||||
|
x -= 1
|
||||||
|
if y > 0 and v < y + 1e-6:
|
||||||
|
y -= 1
|
||||||
|
if x >= 0 and y >= 0:
|
||||||
|
tiles.add((x, y))
|
||||||
|
return tiles
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def currently_image_image_editor(context):
|
def currently_image_image_editor(context):
|
||||||
return isinstance(context.space_data, bpy.types.SpaceImageEditor)
|
return isinstance(context.space_data, bpy.types.SpaceImageEditor)
|
||||||
|
@ -5,19 +5,19 @@
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
def export(filepath, face_data, colors, width, height, opacity):
|
def export(filepath, tile, face_data, colors, width, height, opacity):
|
||||||
with open(filepath, 'w', encoding='utf-8') as file:
|
with open(filepath, 'w', encoding='utf-8') as file:
|
||||||
for text in get_file_parts(face_data, colors, width, height, opacity):
|
for text in get_file_parts(tile, face_data, colors, width, height, opacity):
|
||||||
file.write(text)
|
file.write(text)
|
||||||
|
|
||||||
|
|
||||||
def get_file_parts(face_data, colors, width, height, opacity):
|
def get_file_parts(tile, face_data, colors, width, height, opacity):
|
||||||
yield from header(width, height)
|
yield from header(width, height)
|
||||||
if opacity > 0.0:
|
if opacity > 0.0:
|
||||||
name_by_color = {}
|
name_by_color = {}
|
||||||
yield from prepare_colors(colors, name_by_color)
|
yield from prepare_colors(colors, name_by_color)
|
||||||
yield from draw_colored_polygons(face_data, name_by_color, width, height)
|
yield from draw_colored_polygons(tile, face_data, name_by_color, width, height)
|
||||||
yield from draw_lines(face_data, width, height)
|
yield from draw_lines(tile, face_data, width, height)
|
||||||
yield from footer()
|
yield from footer()
|
||||||
|
|
||||||
|
|
||||||
@ -53,24 +53,24 @@ def prepare_colors(colors, out_name_by_color):
|
|||||||
yield "} def\n"
|
yield "} def\n"
|
||||||
|
|
||||||
|
|
||||||
def draw_colored_polygons(face_data, name_by_color, width, height):
|
def draw_colored_polygons(tile, face_data, name_by_color, width, height):
|
||||||
for uvs, color in face_data:
|
for uvs, color in face_data:
|
||||||
yield from draw_polygon_path(uvs, width, height)
|
yield from draw_polygon_path(tile, uvs, width, height)
|
||||||
yield "closepath\n"
|
yield "closepath\n"
|
||||||
yield "%s\n" % name_by_color[color]
|
yield "%s\n" % name_by_color[color]
|
||||||
|
|
||||||
|
|
||||||
def draw_lines(face_data, width, height):
|
def draw_lines(tile, face_data, width, height):
|
||||||
for uvs, _ in face_data:
|
for uvs, _ in face_data:
|
||||||
yield from draw_polygon_path(uvs, width, height)
|
yield from draw_polygon_path(tile, uvs, width, height)
|
||||||
yield "closepath\n"
|
yield "closepath\n"
|
||||||
yield "stroke\n"
|
yield "stroke\n"
|
||||||
|
|
||||||
|
|
||||||
def draw_polygon_path(uvs, width, height):
|
def draw_polygon_path(tile, uvs, width, height):
|
||||||
yield "newpath\n"
|
yield "newpath\n"
|
||||||
for j, uv in enumerate(uvs):
|
for j, uv in enumerate(uvs):
|
||||||
uv_scale = (uv[0] * width, uv[1] * height)
|
uv_scale = ((uv[0] - tile[0]) * width, (uv[1] - tile[1]) * height)
|
||||||
if j == 0:
|
if j == 0:
|
||||||
yield "%.5f %.5f moveto\n" % uv_scale
|
yield "%.5f %.5f moveto\n" % uv_scale
|
||||||
else:
|
else:
|
||||||
|
@ -15,14 +15,14 @@ except ImportError:
|
|||||||
oiio = None
|
oiio = None
|
||||||
|
|
||||||
|
|
||||||
def export(filepath, face_data, colors, width, height, opacity):
|
def export(filepath, tile, face_data, colors, width, height, opacity):
|
||||||
offscreen = gpu.types.GPUOffScreen(width, height)
|
offscreen = gpu.types.GPUOffScreen(width, height)
|
||||||
offscreen.bind()
|
offscreen.bind()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fb = gpu.state.active_framebuffer_get()
|
fb = gpu.state.active_framebuffer_get()
|
||||||
fb.clear(color=(0.0, 0.0, 0.0, 0.0))
|
fb.clear(color=(0.0, 0.0, 0.0, 0.0))
|
||||||
draw_image(face_data, opacity)
|
draw_image(tile, face_data, opacity)
|
||||||
|
|
||||||
pixel_data = fb.read_color(0, 0, width, height, 4, 0, 'UBYTE')
|
pixel_data = fb.read_color(0, 0, width, height, 4, 0, 'UBYTE')
|
||||||
pixel_data.dimensions = width * height * 4
|
pixel_data.dimensions = width * height * 4
|
||||||
@ -32,11 +32,11 @@ def export(filepath, face_data, colors, width, height, opacity):
|
|||||||
offscreen.free()
|
offscreen.free()
|
||||||
|
|
||||||
|
|
||||||
def draw_image(face_data, opacity):
|
def draw_image(tile, face_data, opacity):
|
||||||
gpu.state.blend_set('ALPHA')
|
gpu.state.blend_set('ALPHA')
|
||||||
|
|
||||||
with gpu.matrix.push_pop():
|
with gpu.matrix.push_pop():
|
||||||
gpu.matrix.load_matrix(get_normalize_uvs_matrix())
|
gpu.matrix.load_matrix(get_normalize_uvs_matrix(tile))
|
||||||
gpu.matrix.load_projection_matrix(Matrix.Identity(4))
|
gpu.matrix.load_projection_matrix(Matrix.Identity(4))
|
||||||
|
|
||||||
draw_background_colors(face_data, opacity)
|
draw_background_colors(face_data, opacity)
|
||||||
@ -45,11 +45,11 @@ def draw_image(face_data, opacity):
|
|||||||
gpu.state.blend_set('NONE')
|
gpu.state.blend_set('NONE')
|
||||||
|
|
||||||
|
|
||||||
def get_normalize_uvs_matrix():
|
def get_normalize_uvs_matrix(tile):
|
||||||
'''matrix maps x and y coordinates from [0, 1] to [-1, 1]'''
|
'''matrix maps x and y coordinates from [0, 1] to [-1, 1]'''
|
||||||
matrix = Matrix.Identity(4)
|
matrix = Matrix.Identity(4)
|
||||||
matrix.col[3][0] = -1
|
matrix.col[3][0] = -1 - (tile[0]) * 2
|
||||||
matrix.col[3][1] = -1
|
matrix.col[3][1] = -1 - (tile[1]) * 2
|
||||||
matrix[0][0] = 2
|
matrix[0][0] = 2
|
||||||
matrix[1][1] = 2
|
matrix[1][1] = 2
|
||||||
|
|
||||||
|
@ -7,15 +7,15 @@ from os.path import basename
|
|||||||
from xml.sax.saxutils import escape
|
from xml.sax.saxutils import escape
|
||||||
|
|
||||||
|
|
||||||
def export(filepath, face_data, colors, width, height, opacity):
|
def export(filepath, tile, face_data, colors, width, height, opacity):
|
||||||
with open(filepath, 'w', encoding='utf-8') as file:
|
with open(filepath, 'w', encoding='utf-8') as file:
|
||||||
for text in get_file_parts(face_data, colors, width, height, opacity):
|
for text in get_file_parts(tile, face_data, colors, width, height, opacity):
|
||||||
file.write(text)
|
file.write(text)
|
||||||
|
|
||||||
|
|
||||||
def get_file_parts(face_data, colors, width, height, opacity):
|
def get_file_parts(tile, face_data, colors, width, height, opacity):
|
||||||
yield from header(width, height)
|
yield from header(width, height)
|
||||||
yield from draw_polygons(face_data, width, height, opacity)
|
yield from draw_polygons(tile, face_data, width, height, opacity)
|
||||||
yield from footer()
|
yield from footer()
|
||||||
|
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ def header(width, height):
|
|||||||
yield f'<desc>{escape(desc)}</desc>\n'
|
yield f'<desc>{escape(desc)}</desc>\n'
|
||||||
|
|
||||||
|
|
||||||
def draw_polygons(face_data, width, height, opacity):
|
def draw_polygons(tile, face_data, width, height, opacity):
|
||||||
for uvs, color in face_data:
|
for uvs, color in face_data:
|
||||||
fill = f'fill="{get_color_string(color)}"'
|
fill = f'fill="{get_color_string(color)}"'
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ def draw_polygons(face_data, width, height, opacity):
|
|||||||
yield ' points="'
|
yield ' points="'
|
||||||
|
|
||||||
for uv in uvs:
|
for uv in uvs:
|
||||||
x, y = uv[0], 1.0 - uv[1]
|
x, y = uv[0] - tile[0], 1.0 - uv[1] + tile[1]
|
||||||
yield f'{x*width:.3f},{y*height:.3f} '
|
yield f'{x*width:.3f},{y*height:.3f} '
|
||||||
yield '" />\n'
|
yield '" />\n'
|
||||||
|
|
||||||
|
@ -1599,17 +1599,6 @@ def save(operator, context, filepath="", scale_factor=1.0, use_scene_unit=False,
|
|||||||
mesh_version.add_variable("mesh", _3ds_uint(3))
|
mesh_version.add_variable("mesh", _3ds_uint(3))
|
||||||
object_info.add_subchunk(mesh_version)
|
object_info.add_subchunk(mesh_version)
|
||||||
|
|
||||||
# Add MASTERSCALE element
|
|
||||||
mscale = _3ds_chunk(MASTERSCALE)
|
|
||||||
mscale.add_variable("scale", _3ds_float(1.0))
|
|
||||||
object_info.add_subchunk(mscale)
|
|
||||||
|
|
||||||
# Add 3D cursor location
|
|
||||||
if use_cursor:
|
|
||||||
cursor_chunk = _3ds_chunk(O_CONSTS)
|
|
||||||
cursor_chunk.add_variable("cursor", _3ds_point_3d(scene.cursor.location))
|
|
||||||
object_info.add_subchunk(cursor_chunk)
|
|
||||||
|
|
||||||
# Init main keyframe data chunk
|
# Init main keyframe data chunk
|
||||||
if use_keyframes:
|
if use_keyframes:
|
||||||
revision = 0x0005
|
revision = 0x0005
|
||||||
@ -1618,92 +1607,6 @@ def save(operator, context, filepath="", scale_factor=1.0, use_scene_unit=False,
|
|||||||
curtime = scene.frame_current
|
curtime = scene.frame_current
|
||||||
kfdata = make_kfdata(revision, start, stop, curtime)
|
kfdata = make_kfdata(revision, start, stop, curtime)
|
||||||
|
|
||||||
# Add AMBIENT color
|
|
||||||
if world is not None and 'WORLD' in object_filter:
|
|
||||||
ambient_chunk = _3ds_chunk(AMBIENTLIGHT)
|
|
||||||
ambient_light = _3ds_chunk(RGB)
|
|
||||||
ambient_light.add_variable("ambient", _3ds_float_color(world.color))
|
|
||||||
ambient_chunk.add_subchunk(ambient_light)
|
|
||||||
object_info.add_subchunk(ambient_chunk)
|
|
||||||
|
|
||||||
# Add BACKGROUND and BITMAP
|
|
||||||
if world.use_nodes:
|
|
||||||
bgtype = 'BACKGROUND'
|
|
||||||
ntree = world.node_tree.links
|
|
||||||
background_color_chunk = _3ds_chunk(RGB)
|
|
||||||
background_chunk = _3ds_chunk(SOLIDBACKGND)
|
|
||||||
background_flag = _3ds_chunk(USE_SOLIDBGND)
|
|
||||||
bgmixer = 'BACKGROUND', 'MIX', 'MIX_RGB'
|
|
||||||
bgshade = 'ADD_SHADER', 'MIX_SHADER', 'OUTPUT_WORLD'
|
|
||||||
bg_tex = 'TEX_IMAGE', 'TEX_ENVIRONMENT'
|
|
||||||
bg_color = next((lk.from_node.inputs[0].default_value[:3] for lk in ntree if lk.from_node.type == bgtype and lk.to_node.type in bgshade), world.color)
|
|
||||||
bg_mixer = next((lk.from_node.type for lk in ntree if lk.from_node.type in bgmixer and lk.to_node.type == bgtype), bgtype)
|
|
||||||
bg_image = next((lk.from_node.image for lk in ntree if lk.from_node.type in bg_tex and lk.to_node.type == bg_mixer), False)
|
|
||||||
gradient = next((lk.from_node.color_ramp.elements for lk in ntree if lk.from_node.type == 'VALTORGB' and lk.to_node.type in bgmixer), False)
|
|
||||||
background_color_chunk.add_variable("color", _3ds_float_color(bg_color))
|
|
||||||
background_chunk.add_subchunk(background_color_chunk)
|
|
||||||
if bg_image:
|
|
||||||
background_image = _3ds_chunk(BITMAP)
|
|
||||||
background_flag = _3ds_chunk(USE_BITMAP)
|
|
||||||
background_image.add_variable("image", _3ds_string(sane_name(bg_image.name)))
|
|
||||||
object_info.add_subchunk(background_image)
|
|
||||||
object_info.add_subchunk(background_chunk)
|
|
||||||
|
|
||||||
# Add VGRADIENT chunk
|
|
||||||
if gradient and len(gradient) >= 3:
|
|
||||||
gradient_chunk = _3ds_chunk(VGRADIENT)
|
|
||||||
background_flag = _3ds_chunk(USE_VGRADIENT)
|
|
||||||
gradient_chunk.add_variable("midpoint", _3ds_float(gradient[1].position))
|
|
||||||
gradient_topcolor_chunk = _3ds_chunk(RGB)
|
|
||||||
gradient_topcolor_chunk.add_variable("color", _3ds_float_color(gradient[2].color[:3]))
|
|
||||||
gradient_chunk.add_subchunk(gradient_topcolor_chunk)
|
|
||||||
gradient_midcolor_chunk = _3ds_chunk(RGB)
|
|
||||||
gradient_midcolor_chunk.add_variable("color", _3ds_float_color(gradient[1].color[:3]))
|
|
||||||
gradient_chunk.add_subchunk(gradient_midcolor_chunk)
|
|
||||||
gradient_lowcolor_chunk = _3ds_chunk(RGB)
|
|
||||||
gradient_lowcolor_chunk.add_variable("color", _3ds_float_color(gradient[0].color[:3]))
|
|
||||||
gradient_chunk.add_subchunk(gradient_lowcolor_chunk)
|
|
||||||
object_info.add_subchunk(gradient_chunk)
|
|
||||||
object_info.add_subchunk(background_flag)
|
|
||||||
|
|
||||||
# Add FOG
|
|
||||||
fognode = next((lk.from_socket.node for lk in ntree if lk.from_socket.node.type == 'VOLUME_ABSORPTION' and lk.to_socket.node.type in bgshade), False)
|
|
||||||
if fognode:
|
|
||||||
fog_chunk = _3ds_chunk(FOG)
|
|
||||||
fog_color_chunk = _3ds_chunk(RGB)
|
|
||||||
use_fog_flag = _3ds_chunk(USE_FOG)
|
|
||||||
fog_density = fognode.inputs['Density'].default_value * 100
|
|
||||||
fog_color_chunk.add_variable("color", _3ds_float_color(fognode.inputs[0].default_value[:3]))
|
|
||||||
fog_chunk.add_variable("nearplane", _3ds_float(world.mist_settings.start))
|
|
||||||
fog_chunk.add_variable("nearfog", _3ds_float(fog_density * 0.5))
|
|
||||||
fog_chunk.add_variable("farplane", _3ds_float(world.mist_settings.depth))
|
|
||||||
fog_chunk.add_variable("farfog", _3ds_float(fog_density + fog_density * 0.5))
|
|
||||||
fog_chunk.add_subchunk(fog_color_chunk)
|
|
||||||
object_info.add_subchunk(fog_chunk)
|
|
||||||
|
|
||||||
# Add LAYER FOG
|
|
||||||
foglayer = next((lk.from_socket.node for lk in ntree if lk.from_socket.node.type == 'VOLUME_SCATTER' and lk.to_socket.node.type in bgshade), False)
|
|
||||||
if foglayer:
|
|
||||||
layerfog_flag = 0
|
|
||||||
if world.mist_settings.falloff == 'QUADRATIC':
|
|
||||||
layerfog_flag |= 0x1
|
|
||||||
if world.mist_settings.falloff == 'INVERSE_QUADRATIC':
|
|
||||||
layerfog_flag |= 0x2
|
|
||||||
layerfog_chunk = _3ds_chunk(LAYER_FOG)
|
|
||||||
layerfog_color_chunk = _3ds_chunk(RGB)
|
|
||||||
use_fog_flag = _3ds_chunk(USE_LAYER_FOG)
|
|
||||||
layerfog_color_chunk.add_variable("color", _3ds_float_color(foglayer.inputs[0].default_value[:3]))
|
|
||||||
layerfog_chunk.add_variable("lowZ", _3ds_float(world.mist_settings.start))
|
|
||||||
layerfog_chunk.add_variable("highZ", _3ds_float(world.mist_settings.height))
|
|
||||||
layerfog_chunk.add_variable("density", _3ds_float(foglayer.inputs[1].default_value))
|
|
||||||
layerfog_chunk.add_variable("flags", _3ds_uint(layerfog_flag))
|
|
||||||
layerfog_chunk.add_subchunk(layerfog_color_chunk)
|
|
||||||
object_info.add_subchunk(layerfog_chunk)
|
|
||||||
if fognode or foglayer and layer.use_pass_mist:
|
|
||||||
object_info.add_subchunk(use_fog_flag)
|
|
||||||
if use_keyframes and world.animation_data or world.node_tree.animation_data:
|
|
||||||
kfdata.add_subchunk(make_ambient_node(world))
|
|
||||||
|
|
||||||
# Make a list of all materials used in the selected meshes (use dictionary, each material is added once)
|
# Make a list of all materials used in the selected meshes (use dictionary, each material is added once)
|
||||||
materialDict = {}
|
materialDict = {}
|
||||||
mesh_objects = []
|
mesh_objects = []
|
||||||
@ -1772,10 +1675,107 @@ def save(operator, context, filepath="", scale_factor=1.0, use_scene_unit=False,
|
|||||||
f.material_index = 0
|
f.material_index = 0
|
||||||
|
|
||||||
|
|
||||||
# Make material chunks for all materials used in the meshes
|
# Make MATERIAL chunks for all materials used in the meshes
|
||||||
for ma_image in materialDict.values():
|
for ma_image in materialDict.values():
|
||||||
object_info.add_subchunk(make_material_chunk(ma_image[0], ma_image[1]))
|
object_info.add_subchunk(make_material_chunk(ma_image[0], ma_image[1]))
|
||||||
|
|
||||||
|
# Add MASTERSCALE element
|
||||||
|
mscale = _3ds_chunk(MASTERSCALE)
|
||||||
|
mscale.add_variable("scale", _3ds_float(1.0))
|
||||||
|
object_info.add_subchunk(mscale)
|
||||||
|
|
||||||
|
# Add 3D cursor location
|
||||||
|
if use_cursor:
|
||||||
|
cursor_chunk = _3ds_chunk(O_CONSTS)
|
||||||
|
cursor_chunk.add_variable("cursor", _3ds_point_3d(scene.cursor.location))
|
||||||
|
object_info.add_subchunk(cursor_chunk)
|
||||||
|
|
||||||
|
# Add AMBIENT color
|
||||||
|
if world is not None and 'WORLD' in object_filter:
|
||||||
|
ambient_chunk = _3ds_chunk(AMBIENTLIGHT)
|
||||||
|
ambient_light = _3ds_chunk(RGB)
|
||||||
|
ambient_light.add_variable("ambient", _3ds_float_color(world.color))
|
||||||
|
ambient_chunk.add_subchunk(ambient_light)
|
||||||
|
object_info.add_subchunk(ambient_chunk)
|
||||||
|
|
||||||
|
# Add BACKGROUND and BITMAP
|
||||||
|
if world.use_nodes:
|
||||||
|
bgtype = 'BACKGROUND'
|
||||||
|
ntree = world.node_tree.links
|
||||||
|
background_color_chunk = _3ds_chunk(RGB)
|
||||||
|
background_chunk = _3ds_chunk(SOLIDBACKGND)
|
||||||
|
background_flag = _3ds_chunk(USE_SOLIDBGND)
|
||||||
|
bgmixer = 'BACKGROUND', 'MIX', 'MIX_RGB'
|
||||||
|
bgshade = 'ADD_SHADER', 'MIX_SHADER', 'OUTPUT_WORLD'
|
||||||
|
bg_tex = 'TEX_IMAGE', 'TEX_ENVIRONMENT'
|
||||||
|
bg_color = next((lk.from_node.inputs[0].default_value[:3] for lk in ntree if lk.from_node.type == bgtype and lk.to_node.type in bgshade), world.color)
|
||||||
|
bg_mixer = next((lk.from_node.type for lk in ntree if lk.from_node.type in bgmixer and lk.to_node.type == bgtype), bgtype)
|
||||||
|
bg_image = next((lk.from_node.image for lk in ntree if lk.from_node.type in bg_tex and lk.to_node.type == bg_mixer), False)
|
||||||
|
gradient = next((lk.from_node.color_ramp.elements for lk in ntree if lk.from_node.type == 'VALTORGB' and lk.to_node.type in bgmixer), False)
|
||||||
|
background_color_chunk.add_variable("color", _3ds_float_color(bg_color))
|
||||||
|
background_chunk.add_subchunk(background_color_chunk)
|
||||||
|
if bg_image and bg_image is not None:
|
||||||
|
background_image = _3ds_chunk(BITMAP)
|
||||||
|
background_flag = _3ds_chunk(USE_BITMAP)
|
||||||
|
background_image.add_variable("image", _3ds_string(sane_name(bg_image.name)))
|
||||||
|
object_info.add_subchunk(background_image)
|
||||||
|
object_info.add_subchunk(background_chunk)
|
||||||
|
|
||||||
|
# Add VGRADIENT chunk
|
||||||
|
if gradient and len(gradient) >= 3:
|
||||||
|
gradient_chunk = _3ds_chunk(VGRADIENT)
|
||||||
|
background_flag = _3ds_chunk(USE_VGRADIENT)
|
||||||
|
gradient_chunk.add_variable("midpoint", _3ds_float(gradient[1].position))
|
||||||
|
gradient_topcolor_chunk = _3ds_chunk(RGB)
|
||||||
|
gradient_topcolor_chunk.add_variable("color", _3ds_float_color(gradient[2].color[:3]))
|
||||||
|
gradient_chunk.add_subchunk(gradient_topcolor_chunk)
|
||||||
|
gradient_midcolor_chunk = _3ds_chunk(RGB)
|
||||||
|
gradient_midcolor_chunk.add_variable("color", _3ds_float_color(gradient[1].color[:3]))
|
||||||
|
gradient_chunk.add_subchunk(gradient_midcolor_chunk)
|
||||||
|
gradient_lowcolor_chunk = _3ds_chunk(RGB)
|
||||||
|
gradient_lowcolor_chunk.add_variable("color", _3ds_float_color(gradient[0].color[:3]))
|
||||||
|
gradient_chunk.add_subchunk(gradient_lowcolor_chunk)
|
||||||
|
object_info.add_subchunk(gradient_chunk)
|
||||||
|
object_info.add_subchunk(background_flag)
|
||||||
|
|
||||||
|
# Add FOG
|
||||||
|
fognode = next((lk.from_socket.node for lk in ntree if lk.from_socket.node.type == 'VOLUME_ABSORPTION' and lk.to_socket.node.type in bgshade), False)
|
||||||
|
if fognode:
|
||||||
|
fog_chunk = _3ds_chunk(FOG)
|
||||||
|
fog_color_chunk = _3ds_chunk(RGB)
|
||||||
|
use_fog_flag = _3ds_chunk(USE_FOG)
|
||||||
|
fog_density = fognode.inputs['Density'].default_value * 100
|
||||||
|
fog_color_chunk.add_variable("color", _3ds_float_color(fognode.inputs[0].default_value[:3]))
|
||||||
|
fog_chunk.add_variable("nearplane", _3ds_float(world.mist_settings.start))
|
||||||
|
fog_chunk.add_variable("nearfog", _3ds_float(fog_density * 0.5))
|
||||||
|
fog_chunk.add_variable("farplane", _3ds_float(world.mist_settings.depth))
|
||||||
|
fog_chunk.add_variable("farfog", _3ds_float(fog_density + fog_density * 0.5))
|
||||||
|
fog_chunk.add_subchunk(fog_color_chunk)
|
||||||
|
object_info.add_subchunk(fog_chunk)
|
||||||
|
|
||||||
|
# Add LAYER FOG
|
||||||
|
foglayer = next((lk.from_socket.node for lk in ntree if lk.from_socket.node.type == 'VOLUME_SCATTER' and lk.to_socket.node.type in bgshade), False)
|
||||||
|
if foglayer:
|
||||||
|
layerfog_flag = 0
|
||||||
|
if world.mist_settings.falloff == 'QUADRATIC':
|
||||||
|
layerfog_flag |= 0x1
|
||||||
|
if world.mist_settings.falloff == 'INVERSE_QUADRATIC':
|
||||||
|
layerfog_flag |= 0x2
|
||||||
|
layerfog_chunk = _3ds_chunk(LAYER_FOG)
|
||||||
|
layerfog_color_chunk = _3ds_chunk(RGB)
|
||||||
|
use_fog_flag = _3ds_chunk(USE_LAYER_FOG)
|
||||||
|
layerfog_color_chunk.add_variable("color", _3ds_float_color(foglayer.inputs[0].default_value[:3]))
|
||||||
|
layerfog_chunk.add_variable("lowZ", _3ds_float(world.mist_settings.start))
|
||||||
|
layerfog_chunk.add_variable("highZ", _3ds_float(world.mist_settings.height))
|
||||||
|
layerfog_chunk.add_variable("density", _3ds_float(foglayer.inputs[1].default_value))
|
||||||
|
layerfog_chunk.add_variable("flags", _3ds_uint(layerfog_flag))
|
||||||
|
layerfog_chunk.add_subchunk(layerfog_color_chunk)
|
||||||
|
object_info.add_subchunk(layerfog_chunk)
|
||||||
|
if fognode or foglayer and layer.use_pass_mist:
|
||||||
|
object_info.add_subchunk(use_fog_flag)
|
||||||
|
if use_keyframes and world.animation_data or (world.node_tree and world.node_tree.animation_data):
|
||||||
|
kfdata.add_subchunk(make_ambient_node(world))
|
||||||
|
|
||||||
# Collect translation for transformation matrix
|
# Collect translation for transformation matrix
|
||||||
translation = {}
|
translation = {}
|
||||||
rotation = {}
|
rotation = {}
|
||||||
@ -1929,11 +1929,10 @@ def save(operator, context, filepath="", scale_factor=1.0, use_scene_unit=False,
|
|||||||
obj_hierarchy_chunk = _3ds_chunk(OBJECT_HIERARCHY)
|
obj_hierarchy_chunk = _3ds_chunk(OBJECT_HIERARCHY)
|
||||||
obj_parent_chunk = _3ds_chunk(OBJECT_PARENT)
|
obj_parent_chunk = _3ds_chunk(OBJECT_PARENT)
|
||||||
obj_hierarchy_chunk.add_variable("hierarchy", _3ds_ushort(object_id[ob.name]))
|
obj_hierarchy_chunk.add_variable("hierarchy", _3ds_ushort(object_id[ob.name]))
|
||||||
if ob.parent is None or (ob.parent.name not in object_id):
|
if ob.parent is not None and (ob.parent.name in object_id):
|
||||||
obj_parent_chunk.add_variable("parent", _3ds_ushort(ROOT_OBJECT))
|
obj_parent_chunk = _3ds_chunk(OBJECT_PARENT)
|
||||||
else: # Get the parent ID from the object_id dict
|
|
||||||
obj_parent_chunk.add_variable("parent", _3ds_ushort(object_id[ob.parent.name]))
|
obj_parent_chunk.add_variable("parent", _3ds_ushort(object_id[ob.parent.name]))
|
||||||
obj_hierarchy_chunk.add_subchunk(obj_parent_chunk)
|
obj_hierarchy_chunk.add_subchunk(obj_parent_chunk)
|
||||||
object_chunk.add_subchunk(obj_hierarchy_chunk)
|
object_chunk.add_subchunk(obj_hierarchy_chunk)
|
||||||
|
|
||||||
# Add light object and hierarchy chunks to object info
|
# Add light object and hierarchy chunks to object info
|
||||||
@ -1967,11 +1966,10 @@ def save(operator, context, filepath="", scale_factor=1.0, use_scene_unit=False,
|
|||||||
obj_hierarchy_chunk = _3ds_chunk(OBJECT_HIERARCHY)
|
obj_hierarchy_chunk = _3ds_chunk(OBJECT_HIERARCHY)
|
||||||
obj_parent_chunk = _3ds_chunk(OBJECT_PARENT)
|
obj_parent_chunk = _3ds_chunk(OBJECT_PARENT)
|
||||||
obj_hierarchy_chunk.add_variable("hierarchy", _3ds_ushort(object_id[ob.name]))
|
obj_hierarchy_chunk.add_variable("hierarchy", _3ds_ushort(object_id[ob.name]))
|
||||||
if ob.parent is None or (ob.parent.name not in object_id):
|
if ob.parent is not None and (ob.parent.name in object_id):
|
||||||
obj_parent_chunk.add_variable("parent", _3ds_ushort(ROOT_OBJECT))
|
obj_parent_chunk = _3ds_chunk(OBJECT_PARENT)
|
||||||
else: # Get the parent ID from the object_id dict
|
|
||||||
obj_parent_chunk.add_variable("parent", _3ds_ushort(object_id[ob.parent.name]))
|
obj_parent_chunk.add_variable("parent", _3ds_ushort(object_id[ob.parent.name]))
|
||||||
obj_hierarchy_chunk.add_subchunk(obj_parent_chunk)
|
obj_hierarchy_chunk.add_subchunk(obj_parent_chunk)
|
||||||
object_chunk.add_subchunk(obj_hierarchy_chunk)
|
object_chunk.add_subchunk(obj_hierarchy_chunk)
|
||||||
|
|
||||||
# Add light object and hierarchy chunks to object info
|
# Add light object and hierarchy chunks to object info
|
||||||
@ -2012,4 +2010,4 @@ def save(operator, context, filepath="", scale_factor=1.0, use_scene_unit=False,
|
|||||||
# Debugging only: dump the chunk hierarchy
|
# Debugging only: dump the chunk hierarchy
|
||||||
# primary.dump()
|
# primary.dump()
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
@ -244,7 +244,7 @@ def skip_to_end(file, skip_chunk):
|
|||||||
# MATERIALS #
|
# MATERIALS #
|
||||||
#############
|
#############
|
||||||
|
|
||||||
def add_texture_to_material(image, contextWrapper, pct, extend, alpha, scale, offset, angle, tintcolor, mapto):
|
def add_texture_to_material(image, contextWrapper, pct, extend, alpha, scale, offset, angle, tint1, tint2, mapto):
|
||||||
shader = contextWrapper.node_principled_bsdf
|
shader = contextWrapper.node_principled_bsdf
|
||||||
nodetree = contextWrapper.material.node_tree
|
nodetree = contextWrapper.material.node_tree
|
||||||
shader.location = (-300, 0)
|
shader.location = (-300, 0)
|
||||||
@ -256,26 +256,31 @@ def add_texture_to_material(image, contextWrapper, pct, extend, alpha, scale, of
|
|||||||
mixer.label = "Mixer"
|
mixer.label = "Mixer"
|
||||||
mixer.inputs[0].default_value = pct / 100
|
mixer.inputs[0].default_value = pct / 100
|
||||||
mixer.inputs[1].default_value = (
|
mixer.inputs[1].default_value = (
|
||||||
tintcolor[:3] + [1] if tintcolor else
|
tint1[:3] + [1] if tint1 else shader.inputs['Base Color'].default_value[:])
|
||||||
shader.inputs['Base Color'].default_value[:]
|
|
||||||
)
|
|
||||||
contextWrapper._grid_to_location(1, 2, dst_node=mixer, ref_node=shader)
|
contextWrapper._grid_to_location(1, 2, dst_node=mixer, ref_node=shader)
|
||||||
img_wrap = contextWrapper.base_color_texture
|
img_wrap = contextWrapper.base_color_texture
|
||||||
links.new(img_wrap.node_image.outputs['Color'], mixer.inputs[2])
|
|
||||||
links.new(mixer.outputs['Color'], shader.inputs['Base Color'])
|
links.new(mixer.outputs['Color'], shader.inputs['Base Color'])
|
||||||
|
if tint2 is not None:
|
||||||
|
img_wrap.colorspace_name = 'Non-Color'
|
||||||
|
mixer.inputs[2].default_value = tint2[:3] + [1]
|
||||||
|
links.new(img_wrap.node_image.outputs['Color'], mixer.inputs[0])
|
||||||
|
else:
|
||||||
|
links.new(img_wrap.node_image.outputs['Color'], mixer.inputs[2])
|
||||||
|
elif mapto == 'ROUGHNESS':
|
||||||
|
img_wrap = contextWrapper.roughness_texture
|
||||||
|
elif mapto == 'METALLIC':
|
||||||
|
shader.location = (300,300)
|
||||||
|
img_wrap = contextWrapper.metallic_texture
|
||||||
elif mapto == 'SPECULARITY':
|
elif mapto == 'SPECULARITY':
|
||||||
|
shader.location = (300,0)
|
||||||
img_wrap = contextWrapper.specular_tint_texture
|
img_wrap = contextWrapper.specular_tint_texture
|
||||||
elif mapto == 'ALPHA':
|
elif mapto == 'ALPHA':
|
||||||
shader.location = (0, -300)
|
shader.location = (-300,0)
|
||||||
img_wrap = contextWrapper.alpha_texture
|
img_wrap = contextWrapper.alpha_texture
|
||||||
elif mapto == 'METALLIC':
|
img_wrap.use_alpha = False
|
||||||
shader.location = (300, 300)
|
links.new(img_wrap.node_image.outputs['Color'], img_wrap.socket_dst)
|
||||||
img_wrap = contextWrapper.metallic_texture
|
|
||||||
elif mapto == 'ROUGHNESS':
|
|
||||||
shader.location = (300, 0)
|
|
||||||
img_wrap = contextWrapper.roughness_texture
|
|
||||||
elif mapto == 'EMISSION':
|
elif mapto == 'EMISSION':
|
||||||
shader.location = (-300, -600)
|
shader.location = (0,-900)
|
||||||
img_wrap = contextWrapper.emission_color_texture
|
img_wrap = contextWrapper.emission_color_texture
|
||||||
elif mapto == 'NORMAL':
|
elif mapto == 'NORMAL':
|
||||||
shader.location = (300, 300)
|
shader.location = (300, 300)
|
||||||
@ -310,10 +315,12 @@ def add_texture_to_material(image, contextWrapper, pct, extend, alpha, scale, of
|
|||||||
img_wrap.extension = 'CLIP'
|
img_wrap.extension = 'CLIP'
|
||||||
|
|
||||||
if alpha == 'alpha':
|
if alpha == 'alpha':
|
||||||
|
own_node = img_wrap.node_image
|
||||||
|
contextWrapper.material.blend_method = 'HASHED'
|
||||||
|
links.new(own_node.outputs['Alpha'], img_wrap.socket_dst)
|
||||||
for link in links:
|
for link in links:
|
||||||
if link.from_node.type == 'TEX_IMAGE' and link.to_node.type == 'MIX_RGB':
|
if link.from_node.type == 'TEX_IMAGE' and link.to_node.type == 'MIX_RGB':
|
||||||
tex = link.from_node.image.name
|
tex = link.from_node.image.name
|
||||||
own_node = img_wrap.node_image
|
|
||||||
own_map = img_wrap.node_mapping
|
own_map = img_wrap.node_mapping
|
||||||
if tex == image.name:
|
if tex == image.name:
|
||||||
links.new(link.from_node.outputs['Alpha'], img_wrap.socket_dst)
|
links.new(link.from_node.outputs['Alpha'], img_wrap.socket_dst)
|
||||||
@ -323,9 +330,6 @@ def add_texture_to_material(image, contextWrapper, pct, extend, alpha, scale, of
|
|||||||
if imgs.name[-3:].isdigit():
|
if imgs.name[-3:].isdigit():
|
||||||
if not imgs.users:
|
if not imgs.users:
|
||||||
bpy.data.images.remove(imgs)
|
bpy.data.images.remove(imgs)
|
||||||
else:
|
|
||||||
links.new(img_wrap.node_image.outputs['Alpha'], img_wrap.socket_dst)
|
|
||||||
contextWrapper.material.blend_method = 'HASHED'
|
|
||||||
|
|
||||||
shader.location = (300, 300)
|
shader.location = (300, 300)
|
||||||
contextWrapper._grid_to_location(1, 0, dst_node=contextWrapper.node_out, ref_node=shader)
|
contextWrapper._grid_to_location(1, 0, dst_node=contextWrapper.node_out, ref_node=shader)
|
||||||
@ -518,7 +522,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
def read_texture(new_chunk, temp_chunk, name, mapto):
|
def read_texture(new_chunk, temp_chunk, name, mapto):
|
||||||
uscale, vscale, uoffset, voffset, angle = 1.0, 1.0, 0.0, 0.0, 0.0
|
uscale, vscale, uoffset, voffset, angle = 1.0, 1.0, 0.0, 0.0, 0.0
|
||||||
contextWrapper.use_nodes = True
|
contextWrapper.use_nodes = True
|
||||||
tintcolor = None
|
tint1 = tint2 = None
|
||||||
extend = 'wrap'
|
extend = 'wrap'
|
||||||
alpha = False
|
alpha = False
|
||||||
pct = 70
|
pct = 70
|
||||||
@ -542,14 +546,8 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
img = load_image(texture_name, dirname, place_holder=False, recursive=IMAGE_SEARCH, check_existing=True)
|
img = load_image(texture_name, dirname, place_holder=False, recursive=IMAGE_SEARCH, check_existing=True)
|
||||||
temp_chunk.bytes_read += read_str_len # plus one for the null character that gets removed
|
temp_chunk.bytes_read += read_str_len # plus one for the null character that gets removed
|
||||||
|
|
||||||
elif temp_chunk.ID == MAT_MAP_USCALE:
|
elif temp_chunk.ID == MAT_BUMP_PERCENT:
|
||||||
uscale = read_float(temp_chunk)
|
contextWrapper.normalmap_strength = (float(read_short(temp_chunk) / 100))
|
||||||
elif temp_chunk.ID == MAT_MAP_VSCALE:
|
|
||||||
vscale = read_float(temp_chunk)
|
|
||||||
elif temp_chunk.ID == MAT_MAP_UOFFSET:
|
|
||||||
uoffset = read_float(temp_chunk)
|
|
||||||
elif temp_chunk.ID == MAT_MAP_VOFFSET:
|
|
||||||
voffset = read_float(temp_chunk)
|
|
||||||
|
|
||||||
elif temp_chunk.ID == MAT_MAP_TILING:
|
elif temp_chunk.ID == MAT_MAP_TILING:
|
||||||
"""Control bit flags, where 0x1 activates decaling, 0x2 activates mirror,
|
"""Control bit flags, where 0x1 activates decaling, 0x2 activates mirror,
|
||||||
@ -578,11 +576,20 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
if tiling & 0x200:
|
if tiling & 0x200:
|
||||||
tint = 'RGBtint'
|
tint = 'RGBtint'
|
||||||
|
|
||||||
|
elif temp_chunk.ID == MAT_MAP_USCALE:
|
||||||
|
uscale = read_float(temp_chunk)
|
||||||
|
elif temp_chunk.ID == MAT_MAP_VSCALE:
|
||||||
|
vscale = read_float(temp_chunk)
|
||||||
|
elif temp_chunk.ID == MAT_MAP_UOFFSET:
|
||||||
|
uoffset = read_float(temp_chunk)
|
||||||
|
elif temp_chunk.ID == MAT_MAP_VOFFSET:
|
||||||
|
voffset = read_float(temp_chunk)
|
||||||
elif temp_chunk.ID == MAT_MAP_ANG:
|
elif temp_chunk.ID == MAT_MAP_ANG:
|
||||||
angle = read_float(temp_chunk)
|
angle = read_float(temp_chunk)
|
||||||
|
|
||||||
elif temp_chunk.ID == MAT_MAP_COL1:
|
elif temp_chunk.ID == MAT_MAP_COL1:
|
||||||
tintcolor = read_byte_color(temp_chunk)
|
tint1 = read_byte_color(temp_chunk)
|
||||||
|
elif temp_chunk.ID == MAT_MAP_COL2:
|
||||||
|
tint2 = read_byte_color(temp_chunk)
|
||||||
|
|
||||||
skip_to_end(file, temp_chunk)
|
skip_to_end(file, temp_chunk)
|
||||||
new_chunk.bytes_read += temp_chunk.bytes_read
|
new_chunk.bytes_read += temp_chunk.bytes_read
|
||||||
@ -590,7 +597,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
# add the map to the material in the right channel
|
# add the map to the material in the right channel
|
||||||
if img:
|
if img:
|
||||||
add_texture_to_material(img, contextWrapper, pct, extend, alpha, (uscale, vscale, 1),
|
add_texture_to_material(img, contextWrapper, pct, extend, alpha, (uscale, vscale, 1),
|
||||||
(uoffset, voffset, 0), angle, tintcolor, mapto)
|
(uoffset, voffset, 0), angle, tint1, tint2, mapto)
|
||||||
|
|
||||||
def apply_constrain(vec):
|
def apply_constrain(vec):
|
||||||
convector = mathutils.Vector.Fill(3, (CONSTRAIN * 0.1))
|
convector = mathutils.Vector.Fill(3, (CONSTRAIN * 0.1))
|
||||||
@ -641,8 +648,6 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
temp_data = file.read(SZ_U_INT * 2)
|
temp_data = file.read(SZ_U_INT * 2)
|
||||||
track_chunk.bytes_read += SZ_U_INT * 2
|
track_chunk.bytes_read += SZ_U_INT * 2
|
||||||
nkeys = read_long(track_chunk)
|
nkeys = read_long(track_chunk)
|
||||||
if nkeys == 0:
|
|
||||||
keyframe_data[0] = default_data
|
|
||||||
for i in range(nkeys):
|
for i in range(nkeys):
|
||||||
nframe = read_long(track_chunk)
|
nframe = read_long(track_chunk)
|
||||||
nflags = read_short(track_chunk)
|
nflags = read_short(track_chunk)
|
||||||
@ -657,8 +662,6 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
temp_data = file.read(SZ_U_SHORT * 5)
|
temp_data = file.read(SZ_U_SHORT * 5)
|
||||||
track_chunk.bytes_read += SZ_U_SHORT * 5
|
track_chunk.bytes_read += SZ_U_SHORT * 5
|
||||||
nkeys = read_long(track_chunk)
|
nkeys = read_long(track_chunk)
|
||||||
if nkeys == 0:
|
|
||||||
keyframe_angle[0] = default_value
|
|
||||||
for i in range(nkeys):
|
for i in range(nkeys):
|
||||||
nframe = read_long(track_chunk)
|
nframe = read_long(track_chunk)
|
||||||
nflags = read_short(track_chunk)
|
nflags = read_short(track_chunk)
|
||||||
@ -815,7 +818,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
if contextWorld is None:
|
if contextWorld is None:
|
||||||
path, filename = os.path.split(file.name)
|
path, filename = os.path.split(file.name)
|
||||||
realname, ext = os.path.splitext(filename)
|
realname, ext = os.path.splitext(filename)
|
||||||
newWorld = bpy.data.worlds.new("Fog: " + realname)
|
contextWorld = bpy.data.worlds.new("Fog: " + realname)
|
||||||
context.scene.world = contextWorld
|
context.scene.world = contextWorld
|
||||||
contextWorld.use_nodes = True
|
contextWorld.use_nodes = True
|
||||||
links = contextWorld.node_tree.links
|
links = contextWorld.node_tree.links
|
||||||
@ -1332,7 +1335,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
|
|
||||||
elif KEYFRAME and new_chunk.ID == COL_TRACK_TAG and tracking == 'AMBIENT': # Ambient
|
elif KEYFRAME and new_chunk.ID == COL_TRACK_TAG and tracking == 'AMBIENT': # Ambient
|
||||||
keyframe_data = {}
|
keyframe_data = {}
|
||||||
default_data = child.color[:]
|
keyframe_data[0] = child.color[:]
|
||||||
child.color = read_track_data(new_chunk)[0]
|
child.color = read_track_data(new_chunk)[0]
|
||||||
ambinode.inputs[0].default_value[:3] = child.color
|
ambinode.inputs[0].default_value[:3] = child.color
|
||||||
ambilite.outputs[0].default_value[:3] = child.color
|
ambilite.outputs[0].default_value[:3] = child.color
|
||||||
@ -1346,7 +1349,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
|
|
||||||
elif KEYFRAME and new_chunk.ID == COL_TRACK_TAG and tracking == 'LIGHT': # Color
|
elif KEYFRAME and new_chunk.ID == COL_TRACK_TAG and tracking == 'LIGHT': # Color
|
||||||
keyframe_data = {}
|
keyframe_data = {}
|
||||||
default_data = child.data.color[:]
|
keyframe_data[0] = child.data.color[:]
|
||||||
child.data.color = read_track_data(new_chunk)[0]
|
child.data.color = read_track_data(new_chunk)[0]
|
||||||
for keydata in keyframe_data.items():
|
for keydata in keyframe_data.items():
|
||||||
child.data.color = keydata[1]
|
child.data.color = keydata[1]
|
||||||
@ -1355,7 +1358,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
|
|
||||||
elif KEYFRAME and new_chunk.ID == POS_TRACK_TAG and tracktype == 'OBJECT': # Translation
|
elif KEYFRAME and new_chunk.ID == POS_TRACK_TAG and tracktype == 'OBJECT': # Translation
|
||||||
keyframe_data = {}
|
keyframe_data = {}
|
||||||
default_data = child.location[:]
|
keyframe_data[0] = child.location[:]
|
||||||
child.location = read_track_data(new_chunk)[0]
|
child.location = read_track_data(new_chunk)[0]
|
||||||
if child.type in {'LIGHT', 'CAMERA'}:
|
if child.type in {'LIGHT', 'CAMERA'}:
|
||||||
trackposition[0] = child.location
|
trackposition[0] = child.location
|
||||||
@ -1384,6 +1387,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
elif KEYFRAME and new_chunk.ID == POS_TRACK_TAG and tracktype == 'TARGET': # Target position
|
elif KEYFRAME and new_chunk.ID == POS_TRACK_TAG and tracktype == 'TARGET': # Target position
|
||||||
keyframe_data = {}
|
keyframe_data = {}
|
||||||
location = child.location
|
location = child.location
|
||||||
|
keyframe_data[0] = trackposition[0]
|
||||||
target = mathutils.Vector(read_track_data(new_chunk)[0])
|
target = mathutils.Vector(read_track_data(new_chunk)[0])
|
||||||
direction = calc_target(location, target)
|
direction = calc_target(location, target)
|
||||||
child.rotation_euler.x = direction[0]
|
child.rotation_euler.x = direction[0]
|
||||||
@ -1407,12 +1411,11 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
|
|
||||||
elif KEYFRAME and new_chunk.ID == ROT_TRACK_TAG and tracktype == 'OBJECT': # Rotation
|
elif KEYFRAME and new_chunk.ID == ROT_TRACK_TAG and tracktype == 'OBJECT': # Rotation
|
||||||
keyframe_rotation = {}
|
keyframe_rotation = {}
|
||||||
|
keyframe_rotation[0] = child.rotation_axis_angle[:]
|
||||||
tflags = read_short(new_chunk)
|
tflags = read_short(new_chunk)
|
||||||
temp_data = file.read(SZ_U_INT * 2)
|
temp_data = file.read(SZ_U_INT * 2)
|
||||||
new_chunk.bytes_read += SZ_U_INT * 2
|
new_chunk.bytes_read += SZ_U_INT * 2
|
||||||
nkeys = read_long(new_chunk)
|
nkeys = read_long(new_chunk)
|
||||||
if nkeys == 0:
|
|
||||||
keyframe_rotation[0] = child.rotation_axis_angle[:]
|
|
||||||
if tflags & 0x8: # Flag 0x8 locks X axis
|
if tflags & 0x8: # Flag 0x8 locks X axis
|
||||||
child.lock_rotation[0] = True
|
child.lock_rotation[0] = True
|
||||||
if tflags & 0x10: # Flag 0x10 locks Y axis
|
if tflags & 0x10: # Flag 0x10 locks Y axis
|
||||||
@ -1445,7 +1448,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
|
|
||||||
elif KEYFRAME and new_chunk.ID == SCL_TRACK_TAG and tracktype == 'OBJECT': # Scale
|
elif KEYFRAME and new_chunk.ID == SCL_TRACK_TAG and tracktype == 'OBJECT': # Scale
|
||||||
keyframe_data = {}
|
keyframe_data = {}
|
||||||
default_data = child.scale[:]
|
keyframe_data[0] = child.scale[:]
|
||||||
child.scale = read_track_data(new_chunk)[0]
|
child.scale = read_track_data(new_chunk)[0]
|
||||||
if contextTrack_flag & 0x8: # Flag 0x8 locks X axis
|
if contextTrack_flag & 0x8: # Flag 0x8 locks X axis
|
||||||
child.lock_scale[0] = True
|
child.lock_scale[0] = True
|
||||||
@ -1465,7 +1468,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
|
|
||||||
elif KEYFRAME and new_chunk.ID == ROLL_TRACK_TAG and tracktype == 'OBJECT': # Roll angle
|
elif KEYFRAME and new_chunk.ID == ROLL_TRACK_TAG and tracktype == 'OBJECT': # Roll angle
|
||||||
keyframe_angle = {}
|
keyframe_angle = {}
|
||||||
default_value = child.rotation_euler.y
|
keyframe_angle[0] = child.rotation_euler.y
|
||||||
child.rotation_euler.y = read_track_angle(new_chunk)[0]
|
child.rotation_euler.y = read_track_angle(new_chunk)[0]
|
||||||
for keydata in keyframe_angle.items():
|
for keydata in keyframe_angle.items():
|
||||||
child.rotation_euler.y = keydata[1]
|
child.rotation_euler.y = keydata[1]
|
||||||
@ -1475,7 +1478,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
|
|
||||||
elif KEYFRAME and new_chunk.ID == FOV_TRACK_TAG and tracking == 'CAMERA': # Field of view
|
elif KEYFRAME and new_chunk.ID == FOV_TRACK_TAG and tracking == 'CAMERA': # Field of view
|
||||||
keyframe_angle = {}
|
keyframe_angle = {}
|
||||||
default_value = child.data.angle
|
keyframe_angle[0] = child.data.angle
|
||||||
child.data.angle = read_track_angle(new_chunk)[0]
|
child.data.angle = read_track_angle(new_chunk)[0]
|
||||||
for keydata in keyframe_angle.items():
|
for keydata in keyframe_angle.items():
|
||||||
child.data.lens = (child.data.sensor_width / 2) / math.tan(keydata[1] / 2)
|
child.data.lens = (child.data.sensor_width / 2) / math.tan(keydata[1] / 2)
|
||||||
@ -1484,7 +1487,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
elif KEYFRAME and new_chunk.ID == HOTSPOT_TRACK_TAG and tracking == 'LIGHT' and spotting == 'SPOT': # Hotspot
|
elif KEYFRAME and new_chunk.ID == HOTSPOT_TRACK_TAG and tracking == 'LIGHT' and spotting == 'SPOT': # Hotspot
|
||||||
keyframe_angle = {}
|
keyframe_angle = {}
|
||||||
cone_angle = math.degrees(child.data.spot_size)
|
cone_angle = math.degrees(child.data.spot_size)
|
||||||
default_value = cone_angle-(child.data.spot_blend * math.floor(cone_angle))
|
keyframe_angle[0] = cone_angle-(child.data.spot_blend * math.floor(cone_angle))
|
||||||
hot_spot = math.degrees(read_track_angle(new_chunk)[0])
|
hot_spot = math.degrees(read_track_angle(new_chunk)[0])
|
||||||
child.data.spot_blend = 1.0 - (hot_spot / cone_angle)
|
child.data.spot_blend = 1.0 - (hot_spot / cone_angle)
|
||||||
for keydata in keyframe_angle.items():
|
for keydata in keyframe_angle.items():
|
||||||
@ -1493,7 +1496,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, CONSTRAI
|
|||||||
|
|
||||||
elif KEYFRAME and new_chunk.ID == FALLOFF_TRACK_TAG and tracking == 'LIGHT' and spotting == 'SPOT': # Falloff
|
elif KEYFRAME and new_chunk.ID == FALLOFF_TRACK_TAG and tracking == 'LIGHT' and spotting == 'SPOT': # Falloff
|
||||||
keyframe_angle = {}
|
keyframe_angle = {}
|
||||||
default_value = math.degrees(child.data.spot_size)
|
keyframe_angle[0] = math.degrees(child.data.spot_size)
|
||||||
child.data.spot_size = read_track_angle(new_chunk)[0]
|
child.data.spot_size = read_track_angle(new_chunk)[0]
|
||||||
for keydata in keyframe_angle.items():
|
for keydata in keyframe_angle.items():
|
||||||
child.data.spot_size = keydata[1]
|
child.data.spot_size = keydata[1]
|
||||||
@ -1732,4 +1735,4 @@ def load(operator, context, filepath="", constrain_size=0.0, use_scene_unit=Fals
|
|||||||
IMAGE_SEARCH=use_image_search, FILTER=object_filter, WORLD_MATRIX=use_world_matrix, KEYFRAME=use_keyframes,
|
IMAGE_SEARCH=use_image_search, FILTER=object_filter, WORLD_MATRIX=use_world_matrix, KEYFRAME=use_keyframes,
|
||||||
APPLY_MATRIX=use_apply_transform, CONVERSE=global_matrix, CURSOR=use_cursor, PIVOT=use_center_pivot,)
|
APPLY_MATRIX=use_apply_transform, CONVERSE=global_matrix, CURSOR=use_cursor, PIVOT=use_center_pivot,)
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
@ -5,7 +5,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "FBX format",
|
"name": "FBX format",
|
||||||
"author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem",
|
"author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem",
|
||||||
"version": (5, 8, 3),
|
"version": (5, 8, 8),
|
||||||
"blender": (3, 6, 0),
|
"blender": (3, 6, 0),
|
||||||
"location": "File > Import-Export",
|
"location": "File > Import-Export",
|
||||||
"description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions",
|
"description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions",
|
||||||
|
@ -250,9 +250,8 @@ class FBXElem:
|
|||||||
for elem in self.elems:
|
for elem in self.elems:
|
||||||
offset = elem._calc_offsets(offset, (elem is elem_last))
|
offset = elem._calc_offsets(offset, (elem is elem_last))
|
||||||
offset += _BLOCK_SENTINEL_LENGTH
|
offset += _BLOCK_SENTINEL_LENGTH
|
||||||
elif not self.props or self.id in _ELEMS_ID_ALWAYS_BLOCK_SENTINEL:
|
elif (not self.props and not is_last) or self.id in _ELEMS_ID_ALWAYS_BLOCK_SENTINEL:
|
||||||
if not is_last:
|
offset += _BLOCK_SENTINEL_LENGTH
|
||||||
offset += _BLOCK_SENTINEL_LENGTH
|
|
||||||
|
|
||||||
return offset
|
return offset
|
||||||
|
|
||||||
@ -282,9 +281,8 @@ class FBXElem:
|
|||||||
assert(elem.id != b'')
|
assert(elem.id != b'')
|
||||||
elem._write(write, tell, (elem is elem_last))
|
elem._write(write, tell, (elem is elem_last))
|
||||||
write(_BLOCK_SENTINEL_DATA)
|
write(_BLOCK_SENTINEL_DATA)
|
||||||
elif not self.props or self.id in _ELEMS_ID_ALWAYS_BLOCK_SENTINEL:
|
elif (not self.props and not is_last) or self.id in _ELEMS_ID_ALWAYS_BLOCK_SENTINEL:
|
||||||
if not is_last:
|
write(_BLOCK_SENTINEL_DATA)
|
||||||
write(_BLOCK_SENTINEL_DATA)
|
|
||||||
|
|
||||||
|
|
||||||
def _write_timedate_hack(elem_root):
|
def _write_timedate_hack(elem_root):
|
||||||
|
@ -1807,18 +1807,16 @@ def fbx_data_armature_elements(root, arm_obj, scene_data):
|
|||||||
elem_data_single_int32(fbx_skin, b"Version", FBX_DEFORMER_SKIN_VERSION)
|
elem_data_single_int32(fbx_skin, b"Version", FBX_DEFORMER_SKIN_VERSION)
|
||||||
elem_data_single_float64(fbx_skin, b"Link_DeformAcuracy", 50.0) # Only vague idea what it is...
|
elem_data_single_float64(fbx_skin, b"Link_DeformAcuracy", 50.0) # Only vague idea what it is...
|
||||||
|
|
||||||
# Pre-process vertex weights (also to check vertices assigned to more than four bones).
|
# Pre-process vertex weights so that the vertices only need to be iterated once.
|
||||||
ob = ob_obj.bdata
|
ob = ob_obj.bdata
|
||||||
bo_vg_idx = {bo_obj.bdata.name: ob.vertex_groups[bo_obj.bdata.name].index
|
bo_vg_idx = {bo_obj.bdata.name: ob.vertex_groups[bo_obj.bdata.name].index
|
||||||
for bo_obj in clusters.keys() if bo_obj.bdata.name in ob.vertex_groups}
|
for bo_obj in clusters.keys() if bo_obj.bdata.name in ob.vertex_groups}
|
||||||
valid_idxs = set(bo_vg_idx.values())
|
valid_idxs = set(bo_vg_idx.values())
|
||||||
vgroups = {vg.index: {} for vg in ob.vertex_groups}
|
vgroups = {vg.index: {} for vg in ob.vertex_groups}
|
||||||
verts_vgroups = (sorted(((vg.group, vg.weight) for vg in v.groups if vg.weight and vg.group in valid_idxs),
|
for idx, v in enumerate(me.vertices):
|
||||||
key=lambda e: e[1], reverse=True)
|
for vg in v.groups:
|
||||||
for v in me.vertices)
|
if (w := vg.weight) and (vg_idx := vg.group) in valid_idxs:
|
||||||
for idx, vgs in enumerate(verts_vgroups):
|
vgroups[vg_idx][idx] = w
|
||||||
for vg_idx, w in vgs:
|
|
||||||
vgroups[vg_idx][idx] = w
|
|
||||||
|
|
||||||
for bo_obj, clstr_key in clusters.items():
|
for bo_obj, clstr_key in clusters.items():
|
||||||
bo = bo_obj.bdata
|
bo = bo_obj.bdata
|
||||||
|
@ -1318,42 +1318,154 @@ class AnimationCurveNodeWrapper:
|
|||||||
min_reldiff_fac = fac * 1.0e-3 # min relative value evolution: 0.1% of current 'order of magnitude'.
|
min_reldiff_fac = fac * 1.0e-3 # min relative value evolution: 0.1% of current 'order of magnitude'.
|
||||||
min_absdiff_fac = 0.1 # A tenth of reldiff...
|
min_absdiff_fac = 0.1 # A tenth of reldiff...
|
||||||
|
|
||||||
are_keyed = []
|
# Initialise to no values enabled for writing.
|
||||||
for values, frame_write_mask in zip(self._frame_values_array, self._frame_write_mask_array):
|
self._frame_write_mask_array[:] = False
|
||||||
# Initialise to no frames written.
|
|
||||||
frame_write_mask[:] = False
|
|
||||||
|
|
||||||
# Create views of the 'previous' and 'current' mask and values. The memoryview, .data, of each array is used
|
# Values are enabled for writing if they differ enough from either of their adjacent values or if they differ
|
||||||
# for its iteration and indexing performance compared to the array.
|
# enough from the closest previous value that is enabled due to either of these conditions.
|
||||||
key = values[1:].data
|
for sampled_values, enabled_mask in zip(self._frame_values_array, self._frame_write_mask_array):
|
||||||
p_key = values[:-1].data
|
# Create overlapping views of the 'previous' (all but the last) and 'current' (all but the first)
|
||||||
key_write = frame_write_mask[1:].data
|
# `sampled_values` and `enabled_mask`.
|
||||||
p_key_write = frame_write_mask[:-1].data
|
# Calculate absolute values from `sampled_values` so that the 'previous' and 'current' absolute arrays can
|
||||||
|
# be views into the same array instead of separately calculated arrays.
|
||||||
|
abs_sampled_values = np.abs(sampled_values)
|
||||||
|
# 'previous' views.
|
||||||
|
p_val_view = sampled_values[:-1]
|
||||||
|
p_abs_val_view = abs_sampled_values[:-1]
|
||||||
|
p_enabled_mask_view = enabled_mask[:-1]
|
||||||
|
# 'current' views.
|
||||||
|
c_val_view = sampled_values[1:]
|
||||||
|
c_abs_val_view = abs_sampled_values[1:]
|
||||||
|
c_enabled_mask_view = enabled_mask[1:]
|
||||||
|
|
||||||
p_keyedval = values[0]
|
# If enough difference from previous sampled value, enable the current value *and* the previous one!
|
||||||
is_keyed = False
|
# The difference check is symmetrical, so this will compare each value to both of its adjacent values.
|
||||||
for idx, (val, p_val) in enumerate(zip(key, p_key)):
|
# Unless it is forcefully enabled later, this is the only way that the first value can be enabled.
|
||||||
if val == p_val:
|
# This is a contracted form of relative + absolute-near-zero difference:
|
||||||
# Never write keyframe when value is exactly the same as prev one!
|
# def is_different(a, b):
|
||||||
continue
|
# abs_diff = abs(a - b)
|
||||||
# This is contracted form of relative + absolute-near-zero difference:
|
# if abs_diff < min_reldiff_fac * min_absdiff_fac:
|
||||||
# absdiff = abs(a - b)
|
# return False
|
||||||
# if absdiff < min_reldiff_fac * min_absdiff_fac:
|
# return (abs_diff / ((abs(a) + abs(b)) / 2)) > min_reldiff_fac
|
||||||
# return False
|
# Note that we ignore the '/ 2' part here, since it's not much significant for us.
|
||||||
# return (absdiff / ((abs(a) + abs(b)) / 2)) > min_reldiff_fac
|
# Contracted form using only builtin Python functions:
|
||||||
# Note that we ignore the '/ 2' part here, since it's not much significant for us.
|
# return abs(a - b) > (min_reldiff_fac * max(abs(a) + abs(b), min_absdiff_fac))
|
||||||
if abs(val - p_val) > (min_reldiff_fac * max(abs(val) + abs(p_val), min_absdiff_fac)):
|
abs_diff = np.abs(c_val_view - p_val_view)
|
||||||
# If enough difference from previous sampled value, key this value *and* the previous one!
|
different_if_greater_than = min_reldiff_fac * np.maximum(c_abs_val_view + p_abs_val_view, min_absdiff_fac)
|
||||||
key_write[idx] = True
|
enough_diff_p_val_mask = abs_diff > different_if_greater_than
|
||||||
p_key_write[idx] = True
|
# Enable both the current values *and* the previous values where `enough_diff_p_val_mask` is True. Some
|
||||||
p_keyedval = val
|
# values may get set to True twice because the views overlap, but this is not a problem.
|
||||||
is_keyed = True
|
p_enabled_mask_view[enough_diff_p_val_mask] = True
|
||||||
elif abs(val - p_keyedval) > (min_reldiff_fac * max((abs(val) + abs(p_keyedval)), min_absdiff_fac)):
|
c_enabled_mask_view[enough_diff_p_val_mask] = True
|
||||||
# Else, if enough difference from previous keyed value, key this value only!
|
|
||||||
key_write[idx] = True
|
# Else, if enough difference from previous enabled value, enable the current value only!
|
||||||
p_keyedval = val
|
# For each 'current' value, get the index of the nearest previous enabled value in `sampled_values` (or
|
||||||
is_keyed = True
|
# itself if the value is enabled).
|
||||||
are_keyed.append(is_keyed)
|
# Start with an array that is the index of the 'current' value in `sampled_values`. The 'current' values are
|
||||||
|
# all but the first value, so the indices will be from 1 to `len(sampled_values)` exclusive.
|
||||||
|
# Let len(sampled_values) == 9:
|
||||||
|
# [1, 2, 3, 4, 5, 6, 7, 8]
|
||||||
|
p_enabled_idx_in_sampled_values = np.arange(1, len(sampled_values))
|
||||||
|
# Replace the indices of all disabled values with 0 in preparation of filling them in with the index of the
|
||||||
|
# nearest previous enabled value. We choose to replace with 0 so that if there is no nearest previous
|
||||||
|
# enabled value, we instead default to `sampled_values[0]`.
|
||||||
|
c_val_disabled_mask = ~c_enabled_mask_view
|
||||||
|
# Let `c_val_disabled_mask` be:
|
||||||
|
# [F, F, T, F, F, T, T, T]
|
||||||
|
# Set indices to 0 where `c_val_disabled_mask` is True:
|
||||||
|
# [1, 2, 3, 4, 5, 6, 7, 8]
|
||||||
|
# v v v v
|
||||||
|
# [1, 2, 0, 4, 5, 0, 0, 0]
|
||||||
|
p_enabled_idx_in_sampled_values[c_val_disabled_mask] = 0
|
||||||
|
# Accumulative maximum travels across the array from left to right, filling in the zeroed indices with the
|
||||||
|
# maximum value so far, which will be the closest previous enabled index because the non-zero indices are
|
||||||
|
# strictly increasing.
|
||||||
|
# [1, 2, 0, 4, 5, 0, 0, 0]
|
||||||
|
# v v v v
|
||||||
|
# [1, 2, 2, 4, 5, 5, 5, 5]
|
||||||
|
p_enabled_idx_in_sampled_values = np.maximum.accumulate(p_enabled_idx_in_sampled_values)
|
||||||
|
# Only disabled values need to be checked against their nearest previous enabled values.
|
||||||
|
# We can additionally ignore all values which equal their immediately previous value because those values
|
||||||
|
# will never be enabled if they were not enabled by the earlier difference check against immediately
|
||||||
|
# previous values.
|
||||||
|
p_enabled_diff_to_check_mask = np.logical_and(c_val_disabled_mask, p_val_view != c_val_view)
|
||||||
|
# Convert from a mask to indices because we need the indices later and because the array of indices will
|
||||||
|
# usually be smaller than the mask array making it faster to index other arrays with.
|
||||||
|
p_enabled_diff_to_check_idx = np.flatnonzero(p_enabled_diff_to_check_mask)
|
||||||
|
# `p_enabled_idx_in_sampled_values` from earlier:
|
||||||
|
# [1, 2, 2, 4, 5, 5, 5, 5]
|
||||||
|
# `p_enabled_diff_to_check_mask` assuming no values equal their immediately previous value:
|
||||||
|
# [F, F, T, F, F, T, T, T]
|
||||||
|
# `p_enabled_diff_to_check_idx`:
|
||||||
|
# [ 2, 5, 6, 7]
|
||||||
|
# `p_enabled_idx_in_sampled_values_to_check`:
|
||||||
|
# [ 2, 5, 5, 5]
|
||||||
|
p_enabled_idx_in_sampled_values_to_check = p_enabled_idx_in_sampled_values[p_enabled_diff_to_check_idx]
|
||||||
|
# Get the 'current' disabled values that need to be checked.
|
||||||
|
c_val_to_check = c_val_view[p_enabled_diff_to_check_idx]
|
||||||
|
c_abs_val_to_check = c_abs_val_view[p_enabled_diff_to_check_idx]
|
||||||
|
# Get the nearest previous enabled value for each value to be checked.
|
||||||
|
nearest_p_enabled_val = sampled_values[p_enabled_idx_in_sampled_values_to_check]
|
||||||
|
abs_nearest_p_enabled_val = np.abs(nearest_p_enabled_val)
|
||||||
|
# Check the relative + absolute-near-zero difference again, but against the nearest previous enabled value
|
||||||
|
# this time.
|
||||||
|
abs_diff = np.abs(c_val_to_check - nearest_p_enabled_val)
|
||||||
|
different_if_greater_than = (min_reldiff_fac
|
||||||
|
* np.maximum(c_abs_val_to_check + abs_nearest_p_enabled_val, min_absdiff_fac))
|
||||||
|
enough_diff_p_enabled_val_mask = abs_diff > different_if_greater_than
|
||||||
|
# If there are any that are different enough from the previous enabled value, then we have to check them all
|
||||||
|
# iteratively because enabling a new value can change the nearest previous enabled value of some elements,
|
||||||
|
# which changes their relative + absolute-near-zero difference:
|
||||||
|
# `p_enabled_diff_to_check_idx`:
|
||||||
|
# [2, 5, 6, 7]
|
||||||
|
# `p_enabled_idx_in_sampled_values_to_check`:
|
||||||
|
# [2, 5, 5, 5]
|
||||||
|
# Let `enough_diff_p_enabled_val_mask` be:
|
||||||
|
# [F, F, T, T]
|
||||||
|
# The first index that is newly enabled is 6:
|
||||||
|
# [2, 5,>6<,5]
|
||||||
|
# But 6 > 5, so the next value's nearest previous enabled index is also affected:
|
||||||
|
# [2, 5, 6,>6<]
|
||||||
|
# We had calculated a newly enabled index of 7 too, but that was calculated against the old nearest previous
|
||||||
|
# enabled index of 5, which has now been updated to 6, so whether 7 is enabled or not needs to be
|
||||||
|
# recalculated:
|
||||||
|
# [F, F, T, ?]
|
||||||
|
if np.any(enough_diff_p_enabled_val_mask):
|
||||||
|
# Accessing .data, the memoryview of the array, iteratively or by individual index is faster than doing
|
||||||
|
# the same with the array itself.
|
||||||
|
zipped = zip(p_enabled_diff_to_check_idx.data,
|
||||||
|
c_val_to_check.data,
|
||||||
|
c_abs_val_to_check.data,
|
||||||
|
p_enabled_idx_in_sampled_values_to_check.data,
|
||||||
|
enough_diff_p_enabled_val_mask.data)
|
||||||
|
# While iterating, we could set updated values into `enough_diff_p_enabled_val_mask` as we go and then
|
||||||
|
# update `enabled_mask` in bulk after the iteration, but if we're going to update an array while
|
||||||
|
# iterating, we may as well update `enabled_mask` directly instead and skip the bulk update.
|
||||||
|
# Additionally, the number of `True` writes to `enabled_mask` is usually much less than the number of
|
||||||
|
# updates that would be required to `enough_diff_p_enabled_val_mask`.
|
||||||
|
c_enabled_mask_view_mv = c_enabled_mask_view.data
|
||||||
|
|
||||||
|
# While iterating, keep track of the most recent newly enabled index, so we can tell when we need to
|
||||||
|
# recalculate whether the current value needs to be enabled.
|
||||||
|
new_p_enabled_idx = -1
|
||||||
|
# Keep track of its value too for performance.
|
||||||
|
new_p_enabled_val = -1
|
||||||
|
new_abs_p_enabled_val = -1
|
||||||
|
for cur_idx, c_val, c_abs_val, old_p_enabled_idx, enough_diff in zipped:
|
||||||
|
if new_p_enabled_idx > old_p_enabled_idx:
|
||||||
|
# The nearest previous enabled value is newly enabled and was not included when
|
||||||
|
# `enough_diff_p_enabled_val_mask` was calculated, so whether the current value is different
|
||||||
|
# enough needs to be recalculated using the newly enabled value.
|
||||||
|
# Check if the relative + absolute-near-zero difference is enough to enable this value.
|
||||||
|
enough_diff = (abs(c_val - new_p_enabled_val)
|
||||||
|
> (min_reldiff_fac * max(c_abs_val + new_abs_p_enabled_val, min_absdiff_fac)))
|
||||||
|
if enough_diff:
|
||||||
|
# The current value needs to be enabled.
|
||||||
|
c_enabled_mask_view_mv[cur_idx] = True
|
||||||
|
# Update the index and values for this newly enabled value.
|
||||||
|
new_p_enabled_idx = cur_idx
|
||||||
|
new_p_enabled_val = c_val
|
||||||
|
new_abs_p_enabled_val = c_abs_val
|
||||||
|
|
||||||
# If we write nothing (action doing nothing) and are in 'force_keep' mode, we key everything! :P
|
# If we write nothing (action doing nothing) and are in 'force_keep' mode, we key everything! :P
|
||||||
# See T41766.
|
# See T41766.
|
||||||
@ -1362,7 +1474,9 @@ class AnimationCurveNodeWrapper:
|
|||||||
# one key in this case.
|
# one key in this case.
|
||||||
# See T41719, T41605, T41254...
|
# See T41719, T41605, T41254...
|
||||||
if self.force_keying or (force_keep and not self):
|
if self.force_keying or (force_keep and not self):
|
||||||
are_keyed[:] = [True] * len(are_keyed)
|
are_keyed = [True] * len(self._frame_write_mask_array)
|
||||||
|
else:
|
||||||
|
are_keyed = np.any(self._frame_write_mask_array, axis=1)
|
||||||
|
|
||||||
# If we did key something, ensure first and last sampled values are keyed as well.
|
# If we did key something, ensure first and last sampled values are keyed as well.
|
||||||
if self.force_startend_keying:
|
if self.force_startend_keying:
|
||||||
|
@ -2780,7 +2780,9 @@ class FbxImportHelperNode:
|
|||||||
for i, w in combined_weights.items():
|
for i, w in combined_weights.items():
|
||||||
indices.append(i)
|
indices.append(i)
|
||||||
if len(w) > 1:
|
if len(w) > 1:
|
||||||
weights.append(sum(w) / len(w))
|
# Add ignored child weights to the current bone's weight.
|
||||||
|
# XXX - Weights that sum to more than 1.0 get clamped to 1.0 when set in the vertex group.
|
||||||
|
weights.append(sum(w))
|
||||||
else:
|
else:
|
||||||
weights.append(w[0])
|
weights.append(w[0])
|
||||||
|
|
||||||
@ -3464,31 +3466,56 @@ def load(operator, context, filepath="",
|
|||||||
def _():
|
def _():
|
||||||
fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxShape'))
|
fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxShape'))
|
||||||
|
|
||||||
|
# - FBX | - Blender equivalent
|
||||||
|
# Mesh | `Mesh`
|
||||||
|
# BlendShape | `Key`
|
||||||
|
# BlendShapeChannel | `ShapeKey`, but without its `.data`.
|
||||||
|
# Shape | `ShapeKey.data`, but also includes normals and the values are relative to the base Mesh
|
||||||
|
# | instead of being absolute. The data is sparse, so each Shape has an "Indexes" array too.
|
||||||
|
# | FBX 2020 introduced 'Modern Style' Shapes that also support tangents, binormals, vertex
|
||||||
|
# | colors and UVs, and can be absolute values instead of relative, but 'Modern Style' Shapes
|
||||||
|
# | are not currently supported.
|
||||||
|
#
|
||||||
|
# The FBX connections between Shapes and Meshes form multiple many-many relationships:
|
||||||
|
# Mesh >-< BlendShape >-< BlendShapeChannel >-< Shape
|
||||||
|
# In practice, the relationships are almost never many-many and are more typically 1-many or 1-1:
|
||||||
|
# Mesh --- BlendShape:
|
||||||
|
# usually 1-1 and the FBX SDK might enforce that each BlendShape is connected to at most one Mesh.
|
||||||
|
# BlendShape --< BlendShapeChannel:
|
||||||
|
# usually 1-many.
|
||||||
|
# BlendShapeChannel --- or uncommonly --< Shape:
|
||||||
|
# usually 1-1, but 1-many is a documented feature.
|
||||||
|
|
||||||
|
def connections_gen(c_src_uuid, fbx_id, fbx_type):
|
||||||
|
"""Helper to reduce duplicate code"""
|
||||||
|
# Rarely, an imported FBX file will have duplicate connections. For Shape Key related connections, FBX
|
||||||
|
# appears to ignore the duplicates, or overwrite the existing duplicates such that the end result is the
|
||||||
|
# same as ignoring them, so keep a set of the seen connections and ignore any duplicates.
|
||||||
|
seen_connections = set()
|
||||||
|
for c_dst_uuid, ctype in fbx_connection_map.get(c_src_uuid, ()):
|
||||||
|
if ctype.props[0] != b'OO':
|
||||||
|
# 'Object-Object' connections only.
|
||||||
|
continue
|
||||||
|
fbx_data, bl_data = fbx_table_nodes.get(c_dst_uuid, (None, None))
|
||||||
|
if fbx_data is None or fbx_data.id != fbx_id or fbx_data.props[2] != fbx_type:
|
||||||
|
# Either `c_dst_uuid` doesn't exist, or it has a different id or type.
|
||||||
|
continue
|
||||||
|
connection_key = (c_src_uuid, c_dst_uuid)
|
||||||
|
if connection_key in seen_connections:
|
||||||
|
# The connection is a duplicate, skip it.
|
||||||
|
continue
|
||||||
|
seen_connections.add(connection_key)
|
||||||
|
yield c_dst_uuid, fbx_data, bl_data
|
||||||
|
|
||||||
mesh_to_shapes = {}
|
mesh_to_shapes = {}
|
||||||
for s_uuid, s_item in fbx_table_nodes.items():
|
for s_uuid, (fbx_sdata, _bl_sdata) in fbx_table_nodes.items():
|
||||||
fbx_sdata, bl_sdata = s_item = fbx_table_nodes.get(s_uuid, (None, None))
|
|
||||||
if fbx_sdata is None or fbx_sdata.id != b'Geometry' or fbx_sdata.props[2] != b'Shape':
|
if fbx_sdata is None or fbx_sdata.id != b'Geometry' or fbx_sdata.props[2] != b'Shape':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# shape -> blendshapechannel -> blendshape -> mesh.
|
# shape -> blendshapechannel -> blendshape -> mesh.
|
||||||
for bc_uuid, bc_ctype in fbx_connection_map.get(s_uuid, ()):
|
for bc_uuid, fbx_bcdata, _bl_bcdata in connections_gen(s_uuid, b'Deformer', b'BlendShapeChannel'):
|
||||||
if bc_ctype.props[0] != b'OO':
|
for bs_uuid, _fbx_bsdata, _bl_bsdata in connections_gen(bc_uuid, b'Deformer', b'BlendShape'):
|
||||||
continue
|
for m_uuid, _fbx_mdata, bl_mdata in connections_gen(bs_uuid, b'Geometry', b'Mesh'):
|
||||||
fbx_bcdata, _bl_bcdata = fbx_table_nodes.get(bc_uuid, (None, None))
|
|
||||||
if fbx_bcdata is None or fbx_bcdata.id != b'Deformer' or fbx_bcdata.props[2] != b'BlendShapeChannel':
|
|
||||||
continue
|
|
||||||
for bs_uuid, bs_ctype in fbx_connection_map.get(bc_uuid, ()):
|
|
||||||
if bs_ctype.props[0] != b'OO':
|
|
||||||
continue
|
|
||||||
fbx_bsdata, _bl_bsdata = fbx_table_nodes.get(bs_uuid, (None, None))
|
|
||||||
if fbx_bsdata is None or fbx_bsdata.id != b'Deformer' or fbx_bsdata.props[2] != b'BlendShape':
|
|
||||||
continue
|
|
||||||
for m_uuid, m_ctype in fbx_connection_map.get(bs_uuid, ()):
|
|
||||||
if m_ctype.props[0] != b'OO':
|
|
||||||
continue
|
|
||||||
fbx_mdata, bl_mdata = fbx_table_nodes.get(m_uuid, (None, None))
|
|
||||||
if fbx_mdata is None or fbx_mdata.id != b'Geometry' or fbx_mdata.props[2] != b'Mesh':
|
|
||||||
continue
|
|
||||||
# Blenmeshes are assumed already created at that time!
|
# Blenmeshes are assumed already created at that time!
|
||||||
assert(isinstance(bl_mdata, bpy.types.Mesh))
|
assert(isinstance(bl_mdata, bpy.types.Mesh))
|
||||||
# Group shapes by mesh so that each mesh only needs to be processed once for all of its shape
|
# Group shapes by mesh so that each mesh only needs to be processed once for all of its shape
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
'name': 'glTF 2.0 format',
|
'name': 'glTF 2.0 format',
|
||||||
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
|
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
|
||||||
"version": (4, 0, 32),
|
"version": (4, 1, 12),
|
||||||
'blender': (4, 0, 0),
|
'blender': (4, 0, 0),
|
||||||
'location': 'File > Import-Export',
|
'location': 'File > Import-Export',
|
||||||
'description': 'Import-Export as glTF 2.0',
|
'description': 'Import-Export as glTF 2.0',
|
||||||
@ -144,13 +144,10 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
'Most efficient and portable, but more difficult to edit later'),
|
'Most efficient and portable, but more difficult to edit later'),
|
||||||
('GLTF_SEPARATE', 'glTF Separate (.gltf + .bin + textures)',
|
('GLTF_SEPARATE', 'glTF Separate (.gltf + .bin + textures)',
|
||||||
'Exports multiple files, with separate JSON, binary and texture data. '
|
'Exports multiple files, with separate JSON, binary and texture data. '
|
||||||
'Easiest to edit later'),
|
'Easiest to edit later')),
|
||||||
('GLTF_EMBEDDED', 'glTF Embedded (.gltf)',
|
|
||||||
'Exports a single file, with all data packed in JSON. '
|
|
||||||
'Less efficient than binary, but easier to edit later')),
|
|
||||||
description=(
|
description=(
|
||||||
'Output format and embedding options. Binary is most efficient, '
|
'Output format. Binary is most efficient, '
|
||||||
'but JSON (embedded or separate) may be easier to edit later'
|
'but JSON may be easier to edit later'
|
||||||
),
|
),
|
||||||
default='GLB', #Warning => If you change the default, need to change the default filter too
|
default='GLB', #Warning => If you change the default, need to change the default filter too
|
||||||
update=on_export_format_changed,
|
update=on_export_format_changed,
|
||||||
@ -174,13 +171,13 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
export_image_format: EnumProperty(
|
export_image_format: EnumProperty(
|
||||||
name='Images',
|
name='Images',
|
||||||
items=(('AUTO', 'Automatic',
|
items=(('AUTO', 'Automatic',
|
||||||
'Save PNGs as PNGs, JPEGs as JPEGs, WEBPs as WEBPs. '
|
'Save PNGs as PNGs, JPEGs as JPEGs, WebPs as WebPs. '
|
||||||
'If neither one, use PNG'),
|
'For other formats, use PNG'),
|
||||||
('JPEG', 'JPEG Format (.jpg)',
|
('JPEG', 'JPEG Format (.jpg)',
|
||||||
'Save images as JPEGs. (Images that need alpha are saved as PNGs though.) '
|
'Save images as JPEGs. (Images that need alpha are saved as PNGs though.) '
|
||||||
'Be aware of a possible loss in quality'),
|
'Be aware of a possible loss in quality'),
|
||||||
('WEBP', 'Webp Format',
|
('WEBP', 'WebP Format',
|
||||||
'Save images as WEBPs as main image (no fallback)'),
|
'Save images as WebPs as main image (no fallback)'),
|
||||||
('NONE', 'None',
|
('NONE', 'None',
|
||||||
'Don\'t export images'),
|
'Don\'t export images'),
|
||||||
),
|
),
|
||||||
@ -192,18 +189,18 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
export_image_add_webp: BoolProperty(
|
export_image_add_webp: BoolProperty(
|
||||||
name='Create Webp',
|
name='Create WebP',
|
||||||
description=(
|
description=(
|
||||||
"Creates webp textures for every textures. "
|
"Creates WebP textures for every texture. "
|
||||||
"For already webp textures, nothing happen"
|
"For already WebP textures, nothing happens"
|
||||||
),
|
),
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
export_image_webp_fallback: BoolProperty(
|
export_image_webp_fallback: BoolProperty(
|
||||||
name='Webp fallback',
|
name='WebP fallback',
|
||||||
description=(
|
description=(
|
||||||
"For all webp textures, create a PNG fallback texture."
|
"For all WebP textures, create a PNG fallback texture"
|
||||||
),
|
),
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
@ -476,6 +473,21 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export_hierarchy_flatten_objs: BoolProperty(
|
||||||
|
name='Flatten Object Hierarchy',
|
||||||
|
description='Flatten Object Hierarchy. Useful in case of non decomposable transformation matrix',
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
export_armature_object_remove: BoolProperty(
|
||||||
|
name='Remove Armature Object',
|
||||||
|
description=(
|
||||||
|
'Remove Armature object if possible.'
|
||||||
|
'If Armature has multiple root bones, object will not be removed'
|
||||||
|
),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
export_optimize_animation_size: BoolProperty(
|
export_optimize_animation_size: BoolProperty(
|
||||||
name='Optimize Animation Size',
|
name='Optimize Animation Size',
|
||||||
description=(
|
description=(
|
||||||
@ -641,7 +653,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
|
|
||||||
export_try_sparse_sk: BoolProperty(
|
export_try_sparse_sk: BoolProperty(
|
||||||
name='Use Sparse Accessor if better',
|
name='Use Sparse Accessor if better',
|
||||||
description='Try using Sparce Accessor if it save space',
|
description='Try using Sparse Accessor if it saves space',
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -653,9 +665,9 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
|
|
||||||
export_gpu_instances: BoolProperty(
|
export_gpu_instances: BoolProperty(
|
||||||
name='GPU Instances',
|
name='GPU Instances',
|
||||||
description='Export using EXT_mesh_gpu_instancing.'
|
description='Export using EXT_mesh_gpu_instancing. '
|
||||||
'Limited to children of a same Empty. '
|
'Limited to children of a given Empty. '
|
||||||
'multiple Materials might be omitted',
|
'Multiple materials might be omitted',
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -821,6 +833,8 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
export_settings['gltf_animations'] = self.export_animations
|
export_settings['gltf_animations'] = self.export_animations
|
||||||
export_settings['gltf_def_bones'] = self.export_def_bones
|
export_settings['gltf_def_bones'] = self.export_def_bones
|
||||||
export_settings['gltf_flatten_bones_hierarchy'] = self.export_hierarchy_flatten_bones
|
export_settings['gltf_flatten_bones_hierarchy'] = self.export_hierarchy_flatten_bones
|
||||||
|
export_settings['gltf_flatten_obj_hierarchy'] = self.export_hierarchy_flatten_objs
|
||||||
|
export_settings['gltf_armature_object_remove'] = self.export_armature_object_remove
|
||||||
if self.export_animations:
|
if self.export_animations:
|
||||||
export_settings['gltf_frame_range'] = self.export_frame_range
|
export_settings['gltf_frame_range'] = self.export_frame_range
|
||||||
export_settings['gltf_force_sampling'] = self.export_force_sampling
|
export_settings['gltf_force_sampling'] = self.export_force_sampling
|
||||||
@ -1054,6 +1068,7 @@ class GLTF_PT_export_data_scene(bpy.types.Panel):
|
|||||||
sfile = context.space_data
|
sfile = context.space_data
|
||||||
operator = sfile.active_operator
|
operator = sfile.active_operator
|
||||||
layout.prop(operator, 'export_gpu_instances')
|
layout.prop(operator, 'export_gpu_instances')
|
||||||
|
layout.prop(operator, 'export_hierarchy_flatten_objs')
|
||||||
|
|
||||||
class GLTF_PT_export_data_mesh(bpy.types.Panel):
|
class GLTF_PT_export_data_mesh(bpy.types.Panel):
|
||||||
bl_space_type = 'FILE_BROWSER'
|
bl_space_type = 'FILE_BROWSER'
|
||||||
@ -1279,6 +1294,8 @@ class GLTF_PT_export_data_armature(bpy.types.Panel):
|
|||||||
if operator.export_force_sampling is False and operator.export_def_bones is True:
|
if operator.export_force_sampling is False and operator.export_def_bones is True:
|
||||||
layout.label(text="Export only deformation bones is not possible when not sampling animation")
|
layout.label(text="Export only deformation bones is not possible when not sampling animation")
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
|
row.prop(operator, 'export_armature_object_remove')
|
||||||
|
row = layout.row()
|
||||||
row.prop(operator, 'export_hierarchy_flatten_bones')
|
row.prop(operator, 'export_hierarchy_flatten_bones')
|
||||||
|
|
||||||
class GLTF_PT_export_data_compression(bpy.types.Panel):
|
class GLTF_PT_export_data_compression(bpy.types.Panel):
|
||||||
@ -1648,7 +1665,7 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper):
|
|||||||
items=(
|
items=(
|
||||||
("BLENDER", "Blender (best for import/export round trip)",
|
("BLENDER", "Blender (best for import/export round trip)",
|
||||||
"Good for re-importing glTFs exported from Blender, "
|
"Good for re-importing glTFs exported from Blender, "
|
||||||
"and re-exporting glTFs to glTFs after Blender editing"
|
"and re-exporting glTFs to glTFs after Blender editing. "
|
||||||
"Bone tips are placed on their local +Y axis (in glTF space)"),
|
"Bone tips are placed on their local +Y axis (in glTF space)"),
|
||||||
("TEMPERANCE", "Temperance (average)",
|
("TEMPERANCE", "Temperance (average)",
|
||||||
"Decent all-around strategy. "
|
"Decent all-around strategy. "
|
||||||
@ -1674,10 +1691,10 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper):
|
|||||||
)
|
)
|
||||||
|
|
||||||
import_webp_texture: BoolProperty(
|
import_webp_texture: BoolProperty(
|
||||||
name='Import Webp textures',
|
name='Import WebP textures',
|
||||||
description=(
|
description=(
|
||||||
"If a texture exists in webp format,"
|
"If a texture exists in WebP format, "
|
||||||
"loads the webp texture instead of the fallback png/jpg one"
|
"loads the WebP texture instead of the fallback PNG/JPEG one"
|
||||||
),
|
),
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
@ -8,6 +8,10 @@ import bpy
|
|||||||
def get_gltf_node_old_name():
|
def get_gltf_node_old_name():
|
||||||
return "glTF Settings"
|
return "glTF Settings"
|
||||||
|
|
||||||
|
# Old group name
|
||||||
|
def get_gltf_old_group_node_name():
|
||||||
|
return "glTF Metallic Roughness"
|
||||||
|
|
||||||
def get_gltf_node_name():
|
def get_gltf_node_name():
|
||||||
return "glTF Material Output"
|
return "glTF Material Output"
|
||||||
|
|
||||||
|
@ -34,7 +34,12 @@ def gather_actions_animations(export_settings):
|
|||||||
|
|
||||||
# Do not manage not exported objects
|
# Do not manage not exported objects
|
||||||
if vtree.nodes[obj_uuid].node is None:
|
if vtree.nodes[obj_uuid].node is None:
|
||||||
continue
|
if export_settings["gltf_armature_object_remove"] is True:
|
||||||
|
# Manage armature object, as this is the object that has the animation
|
||||||
|
if not vtree.nodes[obj_uuid].blender_object:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
animations_, merged_tracks = gather_action_animations(obj_uuid, merged_tracks, len(animations), export_settings)
|
animations_, merged_tracks = gather_action_animations(obj_uuid, merged_tracks, len(animations), export_settings)
|
||||||
animations += animations_
|
animations += animations_
|
||||||
@ -63,7 +68,12 @@ def prepare_actions_range(export_settings):
|
|||||||
|
|
||||||
# Do not manage not exported objects
|
# Do not manage not exported objects
|
||||||
if vtree.nodes[obj_uuid].node is None:
|
if vtree.nodes[obj_uuid].node is None:
|
||||||
continue
|
if export_settings["gltf_armature_object_remove"] is True:
|
||||||
|
# Manage armature object, as this is the object that has the animation
|
||||||
|
if not vtree.nodes[obj_uuid].blender_object:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
if obj_uuid not in export_settings['ranges']:
|
if obj_uuid not in export_settings['ranges']:
|
||||||
export_settings['ranges'][obj_uuid] = {}
|
export_settings['ranges'][obj_uuid] = {}
|
||||||
@ -168,7 +178,12 @@ def prepare_actions_range(export_settings):
|
|||||||
|
|
||||||
# Do not manage not exported objects
|
# Do not manage not exported objects
|
||||||
if vtree.nodes[obj_uuid].node is None:
|
if vtree.nodes[obj_uuid].node is None:
|
||||||
continue
|
if export_settings['gltf_armature_object_remove'] is True:
|
||||||
|
# Manage armature object, as this is the object that has the animation
|
||||||
|
if not vtree.nodes[obj_uuid].blender_object:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
blender_actions = __get_blender_actions(obj_uuid, export_settings)
|
blender_actions = __get_blender_actions(obj_uuid, export_settings)
|
||||||
for blender_action, track, type_ in blender_actions:
|
for blender_action, track, type_ in blender_actions:
|
||||||
|
@ -35,7 +35,12 @@ def gather_scene_animations(export_settings):
|
|||||||
|
|
||||||
# Do not manage not exported objects
|
# Do not manage not exported objects
|
||||||
if vtree.nodes[obj_uuid].node is None:
|
if vtree.nodes[obj_uuid].node is None:
|
||||||
continue
|
if export_settings['gltf_armature_object_remove'] is True:
|
||||||
|
# Manage armature object, as this is the object that has the animation
|
||||||
|
if not vtree.nodes[obj_uuid].blender_object:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
|
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
|
||||||
|
|
||||||
|
@ -22,7 +22,12 @@ def gather_tracks_animations(export_settings):
|
|||||||
|
|
||||||
# Do not manage not exported objects
|
# Do not manage not exported objects
|
||||||
if vtree.nodes[obj_uuid].node is None:
|
if vtree.nodes[obj_uuid].node is None:
|
||||||
continue
|
if export_settings['gltf_armature_object_remove'] is True:
|
||||||
|
# Manage armature object, as this is the object that has the animation
|
||||||
|
if not vtree.nodes[obj_uuid].blender_object:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
animations_, merged_tracks = gather_track_animations(obj_uuid, merged_tracks, len(animations), export_settings)
|
animations_, merged_tracks = gather_track_animations(obj_uuid, merged_tracks, len(animations), export_settings)
|
||||||
animations += animations_
|
animations += animations_
|
||||||
|
@ -119,6 +119,10 @@ def get_cache_data(path: str,
|
|||||||
# Bone has a parent, but in export, after filter, is at root of armature
|
# Bone has a parent, but in export, after filter, is at root of armature
|
||||||
matrix = blender_bone.matrix.copy()
|
matrix = blender_bone.matrix.copy()
|
||||||
|
|
||||||
|
# Because there is no armature object, we need to apply the TRS of armature to the root bone
|
||||||
|
if export_settings['gltf_armature_object_remove'] is True:
|
||||||
|
matrix = matrix @ blender_obj.matrix_world
|
||||||
|
|
||||||
if blender_obj.animation_data and blender_obj.animation_data.action \
|
if blender_obj.animation_data and blender_obj.animation_data.action \
|
||||||
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]:
|
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]:
|
||||||
if blender_bone.name not in data[obj_uuid][blender_obj.animation_data.action.name]['bone'].keys():
|
if blender_bone.name not in data[obj_uuid][blender_obj.animation_data.action.name]['bone'].keys():
|
||||||
|
@ -86,11 +86,8 @@ def __create_buffer(exporter, export_settings):
|
|||||||
if export_settings['gltf_format'] == 'GLB':
|
if export_settings['gltf_format'] == 'GLB':
|
||||||
buffer = exporter.finalize_buffer(export_settings['gltf_filedirectory'], is_glb=True)
|
buffer = exporter.finalize_buffer(export_settings['gltf_filedirectory'], is_glb=True)
|
||||||
else:
|
else:
|
||||||
if export_settings['gltf_format'] == 'GLTF_EMBEDDED':
|
exporter.finalize_buffer(export_settings['gltf_filedirectory'],
|
||||||
exporter.finalize_buffer(export_settings['gltf_filedirectory'])
|
export_settings['gltf_binaryfilename'])
|
||||||
else:
|
|
||||||
exporter.finalize_buffer(export_settings['gltf_filedirectory'],
|
|
||||||
export_settings['gltf_binaryfilename'])
|
|
||||||
|
|
||||||
return buffer
|
return buffer
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from ...io.exp.gltf2_io_user_extensions import export_user_extensions
|
|||||||
from ..com.gltf2_blender_extras import generate_extras
|
from ..com.gltf2_blender_extras import generate_extras
|
||||||
from .gltf2_blender_gather_cache import cached
|
from .gltf2_blender_gather_cache import cached
|
||||||
from . import gltf2_blender_gather_nodes
|
from . import gltf2_blender_gather_nodes
|
||||||
|
from . import gltf2_blender_gather_joints
|
||||||
from . import gltf2_blender_gather_tree
|
from . import gltf2_blender_gather_tree
|
||||||
from .animation.sampled.object.gltf2_blender_gather_object_keyframes import get_cache_data
|
from .animation.sampled.object.gltf2_blender_gather_object_keyframes import get_cache_data
|
||||||
from .animation.gltf2_blender_gather_animations import gather_animations
|
from .animation.gltf2_blender_gather_animations import gather_animations
|
||||||
@ -52,6 +53,8 @@ def __gather_scene(blender_scene, export_settings):
|
|||||||
vtree = gltf2_blender_gather_tree.VExportTree(export_settings)
|
vtree = gltf2_blender_gather_tree.VExportTree(export_settings)
|
||||||
vtree.construct(blender_scene)
|
vtree.construct(blender_scene)
|
||||||
vtree.search_missing_armature() # In case armature are no parented correctly
|
vtree.search_missing_armature() # In case armature are no parented correctly
|
||||||
|
vtree.bake_armature_bone_list() # Used in case we remove the armature
|
||||||
|
vtree.check_if_we_can_remove_armature() # Check if we can remove the armatures objects
|
||||||
|
|
||||||
export_user_extensions('vtree_before_filter_hook', export_settings, vtree)
|
export_user_extensions('vtree_before_filter_hook', export_settings, vtree)
|
||||||
|
|
||||||
@ -59,6 +62,8 @@ def __gather_scene(blender_scene, export_settings):
|
|||||||
vtree.filter()
|
vtree.filter()
|
||||||
if export_settings['gltf_flatten_bones_hierarchy'] is True:
|
if export_settings['gltf_flatten_bones_hierarchy'] is True:
|
||||||
vtree.break_bone_hierarchy()
|
vtree.break_bone_hierarchy()
|
||||||
|
if export_settings['gltf_flatten_obj_hierarchy'] is True:
|
||||||
|
vtree.break_obj_hierarchy()
|
||||||
|
|
||||||
vtree.variants_reset_to_original()
|
vtree.variants_reset_to_original()
|
||||||
|
|
||||||
@ -66,11 +71,41 @@ def __gather_scene(blender_scene, export_settings):
|
|||||||
|
|
||||||
export_settings['vtree'] = vtree
|
export_settings['vtree'] = vtree
|
||||||
|
|
||||||
for r in [vtree.nodes[r] for r in vtree.roots]:
|
|
||||||
node = gltf2_blender_gather_nodes.gather_node(
|
|
||||||
r, export_settings)
|
|
||||||
if node is not None:
|
# If we don't remove armature object, we can't have bones directly at root of scene
|
||||||
scene.nodes.append(node)
|
# So looping only on root nodes, as they are all nodes, not bones
|
||||||
|
if export_settings['gltf_armature_object_remove'] is False:
|
||||||
|
for r in [vtree.nodes[r] for r in vtree.roots]:
|
||||||
|
node = gltf2_blender_gather_nodes.gather_node(
|
||||||
|
r, export_settings)
|
||||||
|
if node is not None:
|
||||||
|
scene.nodes.append(node)
|
||||||
|
else:
|
||||||
|
# If we remove armature objects, we can have bone at root of scene
|
||||||
|
armature_root_joints = {}
|
||||||
|
for r in [vtree.nodes[r] for r in vtree.roots]:
|
||||||
|
# Classic Object/node case
|
||||||
|
if r.blender_type != gltf2_blender_gather_tree.VExportNode.BONE:
|
||||||
|
node = gltf2_blender_gather_nodes.gather_node(
|
||||||
|
r, export_settings)
|
||||||
|
if node is not None:
|
||||||
|
scene.nodes.append(node)
|
||||||
|
else:
|
||||||
|
# We can have bone are root of scene because we remove the armature object
|
||||||
|
# and the armature was at root of scene
|
||||||
|
node = gltf2_blender_gather_joints.gather_joint_vnode(
|
||||||
|
r.uuid, export_settings)
|
||||||
|
if node is not None:
|
||||||
|
scene.nodes.append(node)
|
||||||
|
if r.armature not in armature_root_joints.keys():
|
||||||
|
armature_root_joints[r.armature] = []
|
||||||
|
armature_root_joints[r.armature].append(node)
|
||||||
|
|
||||||
|
# Manage objects parented to bones, now we go through all root objects
|
||||||
|
for k, v in armature_root_joints.items():
|
||||||
|
gltf2_blender_gather_nodes.get_objects_parented_to_bones(k, v, export_settings)
|
||||||
|
|
||||||
vtree.add_neutral_bones()
|
vtree.add_neutral_bones()
|
||||||
|
|
||||||
|
@ -48,11 +48,15 @@ def gather_joint_vnode(vnode, export_settings):
|
|||||||
:return: a glTF2 node (acting as a joint)
|
:return: a glTF2 node (acting as a joint)
|
||||||
"""
|
"""
|
||||||
vtree = export_settings['vtree']
|
vtree = export_settings['vtree']
|
||||||
blender_object = vtree.nodes[vnode].blender_object
|
|
||||||
blender_bone = vtree.nodes[vnode].blender_bone
|
blender_bone = vtree.nodes[vnode].blender_bone
|
||||||
|
|
||||||
|
if export_settings['gltf_armature_object_remove'] is True:
|
||||||
mat = vtree.nodes[vtree.nodes[vnode].parent_uuid].matrix_world.inverted_safe() @ vtree.nodes[vnode].matrix_world
|
if vtree.nodes[vnode].parent_uuid is not None:
|
||||||
|
mat = vtree.nodes[vtree.nodes[vnode].parent_uuid].matrix_world.inverted_safe() @ vtree.nodes[vnode].matrix_world
|
||||||
|
else:
|
||||||
|
mat = vtree.nodes[vnode].matrix_world
|
||||||
|
else:
|
||||||
|
mat = vtree.nodes[vtree.nodes[vnode].parent_uuid].matrix_world.inverted_safe() @ vtree.nodes[vnode].matrix_world
|
||||||
|
|
||||||
trans, rot, sca = mat.decompose()
|
trans, rot, sca = mat.decompose()
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ from . import gltf2_blender_gather_lights
|
|||||||
from .gltf2_blender_gather_tree import VExportNode
|
from .gltf2_blender_gather_tree import VExportNode
|
||||||
|
|
||||||
def gather_node(vnode, export_settings):
|
def gather_node(vnode, export_settings):
|
||||||
|
|
||||||
blender_object = vnode.blender_object
|
blender_object = vnode.blender_object
|
||||||
|
|
||||||
skin = gather_skin(vnode.uuid, export_settings)
|
skin = gather_skin(vnode.uuid, export_settings)
|
||||||
@ -29,7 +30,7 @@ def gather_node(vnode, export_settings):
|
|||||||
|
|
||||||
node = gltf2_io.Node(
|
node = gltf2_io.Node(
|
||||||
camera=__gather_camera(blender_object, export_settings),
|
camera=__gather_camera(blender_object, export_settings),
|
||||||
children=__gather_children(vnode, blender_object, export_settings),
|
children=__gather_children(vnode, export_settings),
|
||||||
extensions=__gather_extensions(blender_object, export_settings),
|
extensions=__gather_extensions(blender_object, export_settings),
|
||||||
extras=__gather_extras(blender_object, export_settings),
|
extras=__gather_extras(blender_object, export_settings),
|
||||||
matrix=__gather_matrix(blender_object, export_settings),
|
matrix=__gather_matrix(blender_object, export_settings),
|
||||||
@ -61,78 +62,103 @@ def __gather_camera(blender_object, export_settings):
|
|||||||
return gltf2_blender_gather_cameras.gather_camera(blender_object.data, export_settings)
|
return gltf2_blender_gather_cameras.gather_camera(blender_object.data, export_settings)
|
||||||
|
|
||||||
|
|
||||||
def __gather_children(vnode, blender_object, export_settings):
|
def __gather_children(vnode, export_settings):
|
||||||
children = []
|
children = []
|
||||||
|
|
||||||
vtree = export_settings['vtree']
|
vtree = export_settings['vtree']
|
||||||
|
|
||||||
# Standard Children / Collection
|
|
||||||
for c in [vtree.nodes[c] for c in vnode.children if vtree.nodes[c].blender_type != gltf2_blender_gather_tree.VExportNode.BONE]:
|
|
||||||
node = gather_node(c, export_settings)
|
|
||||||
if node is not None:
|
|
||||||
children.append(node)
|
|
||||||
|
|
||||||
|
armature_object_uuid = None
|
||||||
|
|
||||||
|
# Standard Children / Collection
|
||||||
|
if export_settings['gltf_armature_object_remove'] is False:
|
||||||
|
for c in [vtree.nodes[c] for c in vnode.children if vtree.nodes[c].blender_type != gltf2_blender_gather_tree.VExportNode.BONE]:
|
||||||
|
node = gather_node(c, export_settings)
|
||||||
|
if node is not None:
|
||||||
|
children.append(node)
|
||||||
|
else:
|
||||||
|
root_joints = []
|
||||||
|
for c in [vtree.nodes[c] for c in vnode.children]:
|
||||||
|
if c.blender_type != gltf2_blender_gather_tree.VExportNode.BONE:
|
||||||
|
node = gather_node(c, export_settings)
|
||||||
|
if node is not None:
|
||||||
|
children.append(node)
|
||||||
|
else:
|
||||||
|
# We come here because armature was remove, and bone can be a child of any object
|
||||||
|
joint = gltf2_blender_gather_joints.gather_joint_vnode(c.uuid, export_settings)
|
||||||
|
children.append(joint)
|
||||||
|
armature_object_uuid = c.armature
|
||||||
|
root_joints.append(joint)
|
||||||
|
|
||||||
|
# Now got all bone children (that are root joints), we can get object parented to bones
|
||||||
|
|
||||||
# Armature --> Retrieve Blender bones
|
# Armature --> Retrieve Blender bones
|
||||||
|
# This can't happen if we remove the Armature Object
|
||||||
if vnode.blender_type == gltf2_blender_gather_tree.VExportNode.ARMATURE:
|
if vnode.blender_type == gltf2_blender_gather_tree.VExportNode.ARMATURE:
|
||||||
|
armature_object_uuid = vnode.uuid
|
||||||
root_joints = []
|
root_joints = []
|
||||||
|
root_bones_uuid = export_settings['vtree'].get_root_bones_uuid(vnode.uuid)
|
||||||
all_armature_children = vnode.children
|
|
||||||
root_bones_uuid = [c for c in all_armature_children if export_settings['vtree'].nodes[c].blender_type == VExportNode.BONE]
|
|
||||||
for bone_uuid in root_bones_uuid:
|
for bone_uuid in root_bones_uuid:
|
||||||
joint = gltf2_blender_gather_joints.gather_joint_vnode(bone_uuid, export_settings)
|
joint = gltf2_blender_gather_joints.gather_joint_vnode(bone_uuid, export_settings)
|
||||||
children.append(joint)
|
children.append(joint)
|
||||||
root_joints.append(joint)
|
root_joints.append(joint)
|
||||||
|
|
||||||
|
|
||||||
|
if vnode.blender_type == gltf2_blender_gather_tree.VExportNode.ARMATURE \
|
||||||
|
or armature_object_uuid is not None:
|
||||||
|
|
||||||
# Object parented to bones
|
# Object parented to bones
|
||||||
direct_bone_children = []
|
get_objects_parented_to_bones(armature_object_uuid, root_joints, export_settings)
|
||||||
for n in [vtree.nodes[i] for i in vtree.get_all_bones(vnode.uuid)]:
|
|
||||||
direct_bone_children.extend([c for c in n.children if vtree.nodes[c].blender_type != gltf2_blender_gather_tree.VExportNode.BONE])
|
|
||||||
|
|
||||||
|
|
||||||
def find_parent_joint(joints, name):
|
|
||||||
for joint in joints:
|
|
||||||
if joint.name == name:
|
|
||||||
return joint
|
|
||||||
parent_joint = find_parent_joint(joint.children, name)
|
|
||||||
if parent_joint:
|
|
||||||
return parent_joint
|
|
||||||
return None
|
|
||||||
|
|
||||||
for child in direct_bone_children: # List of object that are parented to bones
|
|
||||||
# find parent joint
|
|
||||||
parent_joint = find_parent_joint(root_joints, vtree.nodes[child].blender_object.parent_bone)
|
|
||||||
if not parent_joint:
|
|
||||||
continue
|
|
||||||
child_node = gather_node(vtree.nodes[child], export_settings)
|
|
||||||
if child_node is None:
|
|
||||||
continue
|
|
||||||
blender_bone = blender_object.pose.bones[parent_joint.name]
|
|
||||||
|
|
||||||
mat = vtree.nodes[vtree.nodes[child].parent_bone_uuid].matrix_world.inverted_safe() @ vtree.nodes[child].matrix_world
|
|
||||||
loc, rot_quat, scale = mat.decompose()
|
|
||||||
|
|
||||||
trans = __convert_swizzle_location(loc, export_settings)
|
|
||||||
rot = __convert_swizzle_rotation(rot_quat, export_settings)
|
|
||||||
sca = __convert_swizzle_scale(scale, export_settings)
|
|
||||||
|
|
||||||
|
|
||||||
translation, rotation, scale = (None, None, None)
|
|
||||||
if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0:
|
|
||||||
translation = [trans[0], trans[1], trans[2]]
|
|
||||||
if rot[0] != 1.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 0.0:
|
|
||||||
rotation = [rot[1], rot[2], rot[3], rot[0]]
|
|
||||||
if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0:
|
|
||||||
scale = [sca[0], sca[1], sca[2]]
|
|
||||||
|
|
||||||
child_node.translation = translation
|
|
||||||
child_node.rotation = rotation
|
|
||||||
child_node.scale = scale
|
|
||||||
|
|
||||||
parent_joint.children.append(child_node)
|
|
||||||
|
|
||||||
return children
|
return children
|
||||||
|
|
||||||
|
def get_objects_parented_to_bones(armature_object_uuid, root_joints, export_settings):
|
||||||
|
vtree = export_settings['vtree']
|
||||||
|
direct_bone_children = []
|
||||||
|
for n in [vtree.nodes[i] for i in vtree.get_all_bones(armature_object_uuid)]:
|
||||||
|
direct_bone_children.extend([c for c in n.children if vtree.nodes[c].blender_type != gltf2_blender_gather_tree.VExportNode.BONE])
|
||||||
|
|
||||||
|
for child in direct_bone_children: # List of object that are parented to bones
|
||||||
|
# find parent joint
|
||||||
|
parent_joint = __find_parent_joint(root_joints, vtree.nodes[child].blender_object.parent_bone)
|
||||||
|
if not parent_joint:
|
||||||
|
continue
|
||||||
|
child_node = gather_node(vtree.nodes[child], export_settings)
|
||||||
|
if child_node is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
mat = vtree.nodes[vtree.nodes[child].parent_bone_uuid].matrix_world.inverted_safe() @ vtree.nodes[child].matrix_world
|
||||||
|
loc, rot_quat, scale = mat.decompose()
|
||||||
|
|
||||||
|
trans = __convert_swizzle_location(loc, export_settings)
|
||||||
|
rot = __convert_swizzle_rotation(rot_quat, export_settings)
|
||||||
|
sca = __convert_swizzle_scale(scale, export_settings)
|
||||||
|
|
||||||
|
|
||||||
|
translation, rotation, scale = (None, None, None)
|
||||||
|
if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0:
|
||||||
|
translation = [trans[0], trans[1], trans[2]]
|
||||||
|
if rot[0] != 1.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 0.0:
|
||||||
|
rotation = [rot[1], rot[2], rot[3], rot[0]]
|
||||||
|
if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0:
|
||||||
|
scale = [sca[0], sca[1], sca[2]]
|
||||||
|
|
||||||
|
child_node.translation = translation
|
||||||
|
child_node.rotation = rotation
|
||||||
|
child_node.scale = scale
|
||||||
|
|
||||||
|
parent_joint.children.append(child_node)
|
||||||
|
|
||||||
|
|
||||||
|
def __find_parent_joint(joints, name):
|
||||||
|
for joint in joints:
|
||||||
|
if joint.name == name:
|
||||||
|
return joint
|
||||||
|
parent_joint = __find_parent_joint(joint.children, name)
|
||||||
|
if parent_joint:
|
||||||
|
return parent_joint
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def __gather_extensions(blender_object, export_settings):
|
def __gather_extensions(blender_object, export_settings):
|
||||||
extensions = {}
|
extensions = {}
|
||||||
|
@ -7,16 +7,27 @@ from ...io.com import gltf2_io
|
|||||||
from ...io.exp.gltf2_io_user_extensions import export_user_extensions
|
from ...io.exp.gltf2_io_user_extensions import export_user_extensions
|
||||||
from ...io.com.gltf2_io_constants import TextureFilter, TextureWrap
|
from ...io.com.gltf2_io_constants import TextureFilter, TextureWrap
|
||||||
from .gltf2_blender_gather_cache import cached
|
from .gltf2_blender_gather_cache import cached
|
||||||
from .gltf2_blender_get import (
|
from .material.gltf2_blender_search_node_tree import previous_node, previous_socket, get_const_from_socket, NodeSocket
|
||||||
previous_node,
|
|
||||||
previous_socket,
|
|
||||||
get_const_from_socket,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def gather_sampler(blender_shader_node: bpy.types.Node, export_settings):
|
def gather_sampler(blender_shader_node: bpy.types.Node, group_path_str, export_settings):
|
||||||
wrap_s, wrap_t = __gather_wrap(blender_shader_node, export_settings)
|
# reconstruct group_path from group_path_str
|
||||||
|
sep_item = "##~~gltf-sep~~##"
|
||||||
|
sep_inside_item = "##~~gltf-inside-sep~~##"
|
||||||
|
group_path = []
|
||||||
|
tab = group_path_str.split(sep_item)
|
||||||
|
if len(tab) > 0:
|
||||||
|
group_path.append(bpy.data.materials[tab[0]])
|
||||||
|
for idx, i in enumerate(tab[1:]):
|
||||||
|
subtab = i.split(sep_inside_item)
|
||||||
|
if idx == 0:
|
||||||
|
group_path.append(bpy.data.materials[tab[0]].node_tree.nodes[subtab[1]])
|
||||||
|
else:
|
||||||
|
group_path.append(bpy.data.node_groups[subtab[0]].nodes[subtab[1]])
|
||||||
|
|
||||||
|
|
||||||
|
wrap_s, wrap_t = __gather_wrap(blender_shader_node, group_path, export_settings)
|
||||||
|
|
||||||
sampler = gltf2_io.Sampler(
|
sampler = gltf2_io.Sampler(
|
||||||
extensions=__gather_extensions(blender_shader_node, export_settings),
|
extensions=__gather_extensions(blender_shader_node, export_settings),
|
||||||
@ -80,7 +91,7 @@ def __gather_name(blender_shader_node, export_settings):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def __gather_wrap(blender_shader_node, export_settings):
|
def __gather_wrap(blender_shader_node, group_path, export_settings):
|
||||||
# First gather from the Texture node
|
# First gather from the Texture node
|
||||||
if blender_shader_node.extension == 'EXTEND':
|
if blender_shader_node.extension == 'EXTEND':
|
||||||
wrap_s = TextureWrap.ClampToEdge
|
wrap_s = TextureWrap.ClampToEdge
|
||||||
@ -98,7 +109,7 @@ def __gather_wrap(blender_shader_node, export_settings):
|
|||||||
# But still works for old files
|
# But still works for old files
|
||||||
# Still needed for heterogen heterogeneous sampler, like MIRROR x REPEAT, for example
|
# Still needed for heterogen heterogeneous sampler, like MIRROR x REPEAT, for example
|
||||||
# Take manual wrapping into account
|
# Take manual wrapping into account
|
||||||
result = detect_manual_uv_wrapping(blender_shader_node)
|
result = detect_manual_uv_wrapping(blender_shader_node, group_path)
|
||||||
if result:
|
if result:
|
||||||
if result['wrap_s'] is not None: wrap_s = result['wrap_s']
|
if result['wrap_s'] is not None: wrap_s = result['wrap_s']
|
||||||
if result['wrap_t'] is not None: wrap_t = result['wrap_t']
|
if result['wrap_t'] is not None: wrap_t = result['wrap_t']
|
||||||
@ -110,7 +121,7 @@ def __gather_wrap(blender_shader_node, export_settings):
|
|||||||
return wrap_s, wrap_t
|
return wrap_s, wrap_t
|
||||||
|
|
||||||
|
|
||||||
def detect_manual_uv_wrapping(blender_shader_node):
|
def detect_manual_uv_wrapping(blender_shader_node, group_path):
|
||||||
# Detects UV wrapping done using math nodes. This is for emulating wrap
|
# Detects UV wrapping done using math nodes. This is for emulating wrap
|
||||||
# modes Blender doesn't support. It looks like
|
# modes Blender doesn't support. It looks like
|
||||||
#
|
#
|
||||||
@ -124,38 +135,38 @@ def detect_manual_uv_wrapping(blender_shader_node):
|
|||||||
# mode in each direction (or None), and next_socket.
|
# mode in each direction (or None), and next_socket.
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
comb = previous_node(blender_shader_node.inputs['Vector'])
|
comb = previous_node(NodeSocket(blender_shader_node.inputs['Vector'], group_path))
|
||||||
if comb is None or comb.type != 'COMBXYZ': return None
|
if comb.node is None or comb.node.type != 'COMBXYZ': return None
|
||||||
|
|
||||||
for soc in ['X', 'Y']:
|
for soc in ['X', 'Y']:
|
||||||
node = previous_node(comb.inputs[soc])
|
node = previous_node(NodeSocket(comb.node.inputs[soc], comb.group_path))
|
||||||
if node is None: return None
|
if node.node is None: return None
|
||||||
|
|
||||||
if node.type == 'SEPXYZ':
|
if node.node.type == 'SEPXYZ':
|
||||||
# Passed through without change
|
# Passed through without change
|
||||||
wrap = None
|
wrap = None
|
||||||
prev_socket = previous_socket(comb.inputs[soc])
|
prev_socket = previous_socket(NodeSocket(comb.node.inputs[soc], comb.group_path))
|
||||||
elif node.type == 'MATH':
|
elif node.node.type == 'MATH':
|
||||||
# Math node applies a manual wrap
|
# Math node applies a manual wrap
|
||||||
if (node.operation == 'PINGPONG' and
|
if (node.node.operation == 'PINGPONG' and
|
||||||
get_const_from_socket(node.inputs[1], kind='VALUE') == 1.0): # scale = 1
|
get_const_from_socket(NodeSocket(node.node.inputs[1], node.group_path), kind='VALUE') == 1.0): # scale = 1
|
||||||
wrap = TextureWrap.MirroredRepeat
|
wrap = TextureWrap.MirroredRepeat
|
||||||
elif (node.operation == 'WRAP' and
|
elif (node.node.operation == 'WRAP' and
|
||||||
get_const_from_socket(node.inputs[1], kind='VALUE') == 0.0 and # min = 0
|
get_const_from_socket(NodeSocket(node.node.inputs[1], node.group_path), kind='VALUE') == 0.0 and # min = 0
|
||||||
get_const_from_socket(node.inputs[2], kind='VALUE') == 1.0): # max = 1
|
get_const_from_socket(NodeSocket(node.node.inputs[2], node.group_path), kind='VALUE') == 1.0): # max = 1
|
||||||
wrap = TextureWrap.Repeat
|
wrap = TextureWrap.Repeat
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
prev_socket = previous_socket(node.inputs[0])
|
prev_socket = previous_socket(NodeSocket(node.node.inputs[0], node.group_path))
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if prev_socket is None: return None
|
if prev_socket.socket is None: return None
|
||||||
prev_node = prev_socket.node
|
prev_node = prev_socket.socket.node
|
||||||
if prev_node.type != 'SEPXYZ': return None
|
if prev_node.type != 'SEPXYZ': return None
|
||||||
# Make sure X goes to X, etc.
|
# Make sure X goes to X, etc.
|
||||||
if prev_socket.name != soc: return None
|
if prev_socket.socket.name != soc: return None
|
||||||
# Make sure both attach to the same SeparateXYZ node
|
# Make sure both attach to the same SeparateXYZ node
|
||||||
if soc == 'X':
|
if soc == 'X':
|
||||||
sep = prev_node
|
sep = prev_node
|
||||||
@ -164,5 +175,5 @@ def detect_manual_uv_wrapping(blender_shader_node):
|
|||||||
|
|
||||||
result['wrap_s' if soc == 'X' else 'wrap_t'] = wrap
|
result['wrap_s' if soc == 'X' else 'wrap_t'] = wrap
|
||||||
|
|
||||||
result['next_socket'] = sep.inputs[0]
|
result['next_socket'] = NodeSocket(sep.inputs[0], prev_socket.group_path)
|
||||||
return result
|
return result
|
||||||
|
@ -13,6 +13,7 @@ from ...io.com import gltf2_io_constants
|
|||||||
from ...io.exp import gltf2_io_binary_data
|
from ...io.exp import gltf2_io_binary_data
|
||||||
from ..com.gltf2_blender_default import BLENDER_GLTF_SPECIAL_COLLECTION
|
from ..com.gltf2_blender_default import BLENDER_GLTF_SPECIAL_COLLECTION
|
||||||
from . import gltf2_blender_gather_accessors
|
from . import gltf2_blender_gather_accessors
|
||||||
|
from .gltf2_blender_gather_joints import gather_joint_vnode
|
||||||
|
|
||||||
class VExportNode:
|
class VExportNode:
|
||||||
|
|
||||||
@ -76,7 +77,7 @@ class VExportNode:
|
|||||||
def recursive_display(self, tree, mode):
|
def recursive_display(self, tree, mode):
|
||||||
if mode == "simple":
|
if mode == "simple":
|
||||||
for c in self.children:
|
for c in self.children:
|
||||||
print(self.blender_object.name, "/", self.blender_bone.name if self.blender_bone else "", "-->", tree.nodes[c].blender_object.name, "/", tree.nodes[c].blender_bone.name if tree.nodes[c].blender_bone else "" )
|
print(tree.nodes[c].uuid, self.blender_object.name, "/", self.blender_bone.name if self.blender_bone else "", "-->", tree.nodes[c].blender_object.name, "/", tree.nodes[c].blender_bone.name if tree.nodes[c].blender_bone else "" )
|
||||||
tree.nodes[c].recursive_display(tree, mode)
|
tree.nodes[c].recursive_display(tree, mode)
|
||||||
|
|
||||||
class VExportTree:
|
class VExportTree:
|
||||||
@ -278,23 +279,40 @@ class VExportTree:
|
|||||||
def get_all_objects(self):
|
def get_all_objects(self):
|
||||||
return [n.uuid for n in self.nodes.values() if n.blender_type != VExportNode.BONE]
|
return [n.uuid for n in self.nodes.values() if n.blender_type != VExportNode.BONE]
|
||||||
|
|
||||||
def get_all_bones(self, uuid): #For armatue Only
|
def get_all_bones(self, uuid): #For armature only
|
||||||
if self.nodes[uuid].blender_type == VExportNode.ARMATURE:
|
if not hasattr(self.nodes[uuid], "all_bones"):
|
||||||
def recursive_get_all_bones(uuid):
|
if self.nodes[uuid].blender_type == VExportNode.ARMATURE:
|
||||||
total = []
|
def recursive_get_all_bones(uuid):
|
||||||
if self.nodes[uuid].blender_type == VExportNode.BONE:
|
total = []
|
||||||
total.append(uuid)
|
if self.nodes[uuid].blender_type == VExportNode.BONE:
|
||||||
for child_uuid in self.nodes[uuid].children:
|
total.append(uuid)
|
||||||
total.extend(recursive_get_all_bones(child_uuid))
|
for child_uuid in self.nodes[uuid].children:
|
||||||
|
total.extend(recursive_get_all_bones(child_uuid))
|
||||||
|
|
||||||
return total
|
return total
|
||||||
|
|
||||||
tot = []
|
tot = []
|
||||||
for c_uuid in self.nodes[uuid].children:
|
for c_uuid in self.nodes[uuid].children:
|
||||||
tot.extend(recursive_get_all_bones(c_uuid))
|
tot.extend(recursive_get_all_bones(c_uuid))
|
||||||
return tot
|
self.nodes[uuid].all_bones = tot
|
||||||
|
return tot # Not really needed to return, we are just baking it before export really starts
|
||||||
|
else:
|
||||||
|
self.nodes[uuid].all_bones = []
|
||||||
|
return []
|
||||||
else:
|
else:
|
||||||
return []
|
return self.nodes[uuid].all_bones
|
||||||
|
|
||||||
|
def get_root_bones_uuid(self, uuid): #For armature only
|
||||||
|
if not hasattr(self.nodes[uuid], "root_bones_uuid"):
|
||||||
|
if self.nodes[uuid].blender_type == VExportNode.ARMATURE:
|
||||||
|
all_armature_children = self.nodes[uuid].children
|
||||||
|
self.nodes[uuid].root_bones_uuid = [c for c in all_armature_children if self.nodes[c].blender_type == VExportNode.BONE]
|
||||||
|
return self.nodes[uuid].root_bones_uuid # Not really needed to return, we are just baking it before export really starts
|
||||||
|
else:
|
||||||
|
self.nodes[uuid].root_bones_uuid = []
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
return self.nodes[uuid].root_bones_uuid
|
||||||
|
|
||||||
def get_all_node_of_type(self, node_type):
|
def get_all_node_of_type(self, node_type):
|
||||||
return [n.uuid for n in self.nodes.values() if n.blender_type == node_type]
|
return [n.uuid for n in self.nodes.values() if n.blender_type == node_type]
|
||||||
@ -302,10 +320,9 @@ class VExportTree:
|
|||||||
def display(self, mode):
|
def display(self, mode):
|
||||||
if mode == "simple":
|
if mode == "simple":
|
||||||
for n in self.roots:
|
for n in self.roots:
|
||||||
print("Root", self.nodes[n].blender_object.name, "/", self.nodes[n].blender_bone.name if self.nodes[n].blender_bone else "" )
|
print(self.nodes[n].uuid, "Root", self.nodes[n].blender_object.name, "/", self.nodes[n].blender_bone.name if self.nodes[n].blender_bone else "" )
|
||||||
self.nodes[n].recursive_display(self, mode)
|
self.nodes[n].recursive_display(self, mode)
|
||||||
|
|
||||||
|
|
||||||
def filter_tag(self):
|
def filter_tag(self):
|
||||||
roots = self.roots.copy()
|
roots = self.roots.copy()
|
||||||
for r in roots:
|
for r in roots:
|
||||||
@ -322,7 +339,6 @@ class VExportTree:
|
|||||||
self.filter_perform()
|
self.filter_perform()
|
||||||
self.remove_filtered_nodes()
|
self.remove_filtered_nodes()
|
||||||
|
|
||||||
|
|
||||||
def recursive_filter_tag(self, uuid, parent_keep_tag):
|
def recursive_filter_tag(self, uuid, parent_keep_tag):
|
||||||
# parent_keep_tag is for collection instance
|
# parent_keep_tag is for collection instance
|
||||||
# some properties (selection, visibility, renderability)
|
# some properties (selection, visibility, renderability)
|
||||||
@ -442,10 +458,20 @@ class VExportTree:
|
|||||||
bpy.data.collections[BLENDER_GLTF_SPECIAL_COLLECTION].objects:
|
bpy.data.collections[BLENDER_GLTF_SPECIAL_COLLECTION].objects:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if self.export_settings['gltf_armature_object_remove'] is True:
|
||||||
|
# If we remove the Armature object
|
||||||
|
if self.nodes[uuid].blender_type == VExportNode.ARMATURE:
|
||||||
|
self.nodes[uuid].arma_exported = True
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def remove_filtered_nodes(self):
|
def remove_filtered_nodes(self):
|
||||||
self.nodes = {k:n for (k, n) in self.nodes.items() if n.keep_tag is True}
|
if self.export_settings['gltf_armature_object_remove'] is True:
|
||||||
|
# If we remove the Armature object
|
||||||
|
self.nodes = {k:n for (k, n) in self.nodes.items() if n.keep_tag is True or (n.keep_tag is False and n.blender_type == VExportNode.ARMATURE)}
|
||||||
|
else:
|
||||||
|
self.nodes = {k:n for (k, n) in self.nodes.items() if n.keep_tag is True}
|
||||||
|
|
||||||
def search_missing_armature(self):
|
def search_missing_armature(self):
|
||||||
for n in [n for n in self.nodes.values() if hasattr(n, "armature_needed") is True]:
|
for n in [n for n in self.nodes.values() if hasattr(n, "armature_needed") is True]:
|
||||||
@ -454,6 +480,14 @@ class VExportTree:
|
|||||||
n.armature = candidates[0].uuid
|
n.armature = candidates[0].uuid
|
||||||
del n.armature_needed
|
del n.armature_needed
|
||||||
|
|
||||||
|
def bake_armature_bone_list(self):
|
||||||
|
# Used to store data in armature vnode
|
||||||
|
# If armature is removed from export
|
||||||
|
# Data are still available, even if armature is not exported (so bones are re-parented)
|
||||||
|
for n in [n for n in self.nodes.values() if n.blender_type == VExportNode.ARMATURE]:
|
||||||
|
self.get_all_bones(n.uuid)
|
||||||
|
self.get_root_bones_uuid(n.uuid)
|
||||||
|
|
||||||
def add_neutral_bones(self):
|
def add_neutral_bones(self):
|
||||||
added_armatures = []
|
added_armatures = []
|
||||||
for n in [n for n in self.nodes.values() if \
|
for n in [n for n in self.nodes.values() if \
|
||||||
@ -521,6 +555,9 @@ class VExportTree:
|
|||||||
from .gltf2_blender_gather_skins import gather_skin
|
from .gltf2_blender_gather_skins import gather_skin
|
||||||
skins = []
|
skins = []
|
||||||
for n in [n for n in self.nodes.values() if n.blender_type == VExportNode.ARMATURE]:
|
for n in [n for n in self.nodes.values() if n.blender_type == VExportNode.ARMATURE]:
|
||||||
|
if self.export_settings['gltf_armature_object_remove'] is True:
|
||||||
|
if hasattr(n, "arma_exported") is False:
|
||||||
|
continue
|
||||||
if len([m for m in self.nodes.values() if m.keep_tag is True and m.blender_type == VExportNode.OBJECT and m.armature == n.uuid]) == 0:
|
if len([m for m in self.nodes.values() if m.keep_tag is True and m.blender_type == VExportNode.OBJECT and m.armature == n.uuid]) == 0:
|
||||||
skin = gather_skin(n.uuid, self.export_settings)
|
skin = gather_skin(n.uuid, self.export_settings)
|
||||||
skins.append(skin)
|
skins.append(skin)
|
||||||
@ -552,3 +589,25 @@ class VExportTree:
|
|||||||
self.nodes[self.nodes[bone].parent_uuid].children.remove(bone)
|
self.nodes[self.nodes[bone].parent_uuid].children.remove(bone)
|
||||||
self.nodes[bone].parent_uuid = arma
|
self.nodes[bone].parent_uuid = arma
|
||||||
self.nodes[arma].children.append(bone)
|
self.nodes[arma].children.append(bone)
|
||||||
|
|
||||||
|
def break_obj_hierarchy(self):
|
||||||
|
# Can be usefull when matrix is not decomposable
|
||||||
|
# TODO: if we get real collection one day, we probably need to adapt this code
|
||||||
|
for obj in self.get_all_objects():
|
||||||
|
if self.nodes[obj].armature is not None and self.nodes[obj].parent_uuid == self.nodes[obj].armature:
|
||||||
|
continue # Keep skined meshs as children of armature
|
||||||
|
if self.nodes[obj].parent_uuid is not None:
|
||||||
|
self.nodes[self.nodes[obj].parent_uuid].children.remove(obj)
|
||||||
|
self.nodes[obj].parent_uuid = None
|
||||||
|
self.roots.append(obj)
|
||||||
|
|
||||||
|
def check_if_we_can_remove_armature(self):
|
||||||
|
# If user requested to remove armature, we need to check if it is possible
|
||||||
|
# If is impossible to remove it if armature has multiple root bones. (glTF validator error)
|
||||||
|
# Currently, we manage it at export level, not at each armature level
|
||||||
|
for arma_uuid in [n for n in self.nodes.keys() if self.nodes[n].blender_type == VExportNode.ARMATURE]:
|
||||||
|
if len(self.get_root_bones_uuid(arma_uuid)) > 1:
|
||||||
|
# We can't remove armature
|
||||||
|
self.export_settings['gltf_armature_object_remove'] = False
|
||||||
|
print("WARNING: We can't remove armature object because some armatures have multiple root bones.")
|
||||||
|
break
|
||||||
|
@ -3,12 +3,6 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from mathutils import Vector, Matrix
|
|
||||||
from ...blender.com.gltf2_blender_conversion import texture_transform_blender_to_gltf
|
|
||||||
from ...io.com import gltf2_io_debug
|
|
||||||
from ..com.gltf2_blender_material_helpers import get_gltf_node_name, get_gltf_node_old_name
|
|
||||||
from .material import gltf2_blender_search_node_tree
|
|
||||||
|
|
||||||
|
|
||||||
def get_animation_target(action_group: bpy.types.ActionGroup):
|
def get_animation_target(action_group: bpy.types.ActionGroup):
|
||||||
return action_group.channels[0].data_path.split('.')[-1]
|
return action_group.channels[0].data_path.split('.')[-1]
|
||||||
@ -31,292 +25,3 @@ def get_object_from_datapath(blender_object, data_path: str):
|
|||||||
# path_attr = data_path
|
# path_attr = data_path
|
||||||
|
|
||||||
return prop
|
return prop
|
||||||
|
|
||||||
|
|
||||||
def get_node_socket(blender_material, type, name):
|
|
||||||
"""
|
|
||||||
For a given material input name, retrieve the corresponding node tree socket for a given node type.
|
|
||||||
|
|
||||||
:param blender_material: a blender material for which to get the socket
|
|
||||||
:return: a blender NodeSocket for a given type
|
|
||||||
"""
|
|
||||||
nodes = [n for n in blender_material.node_tree.nodes if isinstance(n, type) and not n.mute]
|
|
||||||
nodes = [node for node in nodes if check_if_is_linked_to_active_output(node.outputs[0])]
|
|
||||||
inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], [])
|
|
||||||
if inputs:
|
|
||||||
return inputs[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_socket(blender_material: bpy.types.Material, name: str, volume=False):
|
|
||||||
"""
|
|
||||||
For a given material input name, retrieve the corresponding node tree socket.
|
|
||||||
|
|
||||||
:param blender_material: a blender material for which to get the socket
|
|
||||||
:param name: the name of the socket
|
|
||||||
:return: a blender NodeSocket
|
|
||||||
"""
|
|
||||||
if blender_material.node_tree and blender_material.use_nodes:
|
|
||||||
#i = [input for input in blender_material.node_tree.inputs]
|
|
||||||
#o = [output for output in blender_material.node_tree.outputs]
|
|
||||||
if name == "Emissive":
|
|
||||||
# Check for a dedicated Emission node first, it must supersede the newer built-in one
|
|
||||||
# because the newer one is always present in all Principled BSDF materials.
|
|
||||||
emissive_socket = get_node_socket(blender_material, bpy.types.ShaderNodeEmission, "Color")
|
|
||||||
if emissive_socket:
|
|
||||||
return emissive_socket
|
|
||||||
# If a dedicated Emission node was not found, fall back to the Principled BSDF Emission Color socket.
|
|
||||||
name = "Emission Color"
|
|
||||||
type = bpy.types.ShaderNodeBsdfPrincipled
|
|
||||||
elif name == "Background":
|
|
||||||
type = bpy.types.ShaderNodeBackground
|
|
||||||
name = "Color"
|
|
||||||
else:
|
|
||||||
if volume is False:
|
|
||||||
type = bpy.types.ShaderNodeBsdfPrincipled
|
|
||||||
else:
|
|
||||||
type = bpy.types.ShaderNodeVolumeAbsorption
|
|
||||||
|
|
||||||
return get_node_socket(blender_material, type, name)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_socket_old(blender_material: bpy.types.Material, name: str):
|
|
||||||
"""
|
|
||||||
For a given material input name, retrieve the corresponding node tree socket in the special glTF node group.
|
|
||||||
|
|
||||||
:param blender_material: a blender material for which to get the socket
|
|
||||||
:param name: the name of the socket
|
|
||||||
:return: a blender NodeSocket
|
|
||||||
"""
|
|
||||||
gltf_node_group_names = [get_gltf_node_name().lower(), get_gltf_node_old_name().lower()]
|
|
||||||
if blender_material.node_tree and blender_material.use_nodes:
|
|
||||||
# Some weird node groups with missing datablock can have no node_tree, so checking n.node_tree (See #1797)
|
|
||||||
nodes = [n for n in blender_material.node_tree.nodes if \
|
|
||||||
isinstance(n, bpy.types.ShaderNodeGroup) and \
|
|
||||||
n.node_tree is not None and
|
|
||||||
(n.node_tree.name.startswith('glTF Metallic Roughness') or n.node_tree.name.lower() in gltf_node_group_names)]
|
|
||||||
inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], [])
|
|
||||||
if inputs:
|
|
||||||
return inputs[0]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def check_if_is_linked_to_active_output(shader_socket):
|
|
||||||
for link in shader_socket.links:
|
|
||||||
if isinstance(link.to_node, bpy.types.ShaderNodeOutputMaterial) and link.to_node.is_active_output is True:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if len(link.to_node.outputs) > 0: # ignore non active output, not having output sockets
|
|
||||||
ret = check_if_is_linked_to_active_output(link.to_node.outputs[0]) # recursive until find an output material node
|
|
||||||
if ret is True:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def find_shader_image_from_shader_socket(shader_socket, max_hops=10):
|
|
||||||
"""Find any ShaderNodeTexImage in the path from the socket."""
|
|
||||||
if shader_socket is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if max_hops <= 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
for link in shader_socket.links:
|
|
||||||
if isinstance(link.from_node, bpy.types.ShaderNodeTexImage):
|
|
||||||
return link.from_node
|
|
||||||
|
|
||||||
for socket in link.from_node.inputs.values():
|
|
||||||
image = find_shader_image_from_shader_socket(shader_socket=socket, max_hops=max_hops - 1)
|
|
||||||
if image is not None:
|
|
||||||
return image
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_texture_transform_from_mapping_node(mapping_node):
|
|
||||||
if mapping_node.vector_type not in ["TEXTURE", "POINT", "VECTOR"]:
|
|
||||||
gltf2_io_debug.print_console("WARNING",
|
|
||||||
"Skipping exporting texture transform because it had type " +
|
|
||||||
mapping_node.vector_type + "; recommend using POINT instead"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
rotation_0, rotation_1 = mapping_node.inputs['Rotation'].default_value[0], mapping_node.inputs['Rotation'].default_value[1]
|
|
||||||
if rotation_0 or rotation_1:
|
|
||||||
# TODO: can we handle this?
|
|
||||||
gltf2_io_debug.print_console("WARNING",
|
|
||||||
"Skipping exporting texture transform because it had non-zero "
|
|
||||||
"rotations in the X/Y direction; only a Z rotation can be exported!"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
mapping_transform = {}
|
|
||||||
mapping_transform["offset"] = [mapping_node.inputs['Location'].default_value[0], mapping_node.inputs['Location'].default_value[1]]
|
|
||||||
mapping_transform["rotation"] = mapping_node.inputs['Rotation'].default_value[2]
|
|
||||||
mapping_transform["scale"] = [mapping_node.inputs['Scale'].default_value[0], mapping_node.inputs['Scale'].default_value[1]]
|
|
||||||
|
|
||||||
if mapping_node.vector_type == "TEXTURE":
|
|
||||||
# This means use the inverse of the TRS transform.
|
|
||||||
def inverted(mapping_transform):
|
|
||||||
offset = mapping_transform["offset"]
|
|
||||||
rotation = mapping_transform["rotation"]
|
|
||||||
scale = mapping_transform["scale"]
|
|
||||||
|
|
||||||
# Inverse of a TRS is not always a TRS. This function will be right
|
|
||||||
# at least when the following don't occur.
|
|
||||||
if abs(rotation) > 1e-5 and abs(scale[0] - scale[1]) > 1e-5:
|
|
||||||
return None
|
|
||||||
if abs(scale[0]) < 1e-5 or abs(scale[1]) < 1e-5:
|
|
||||||
return None
|
|
||||||
|
|
||||||
new_offset = Matrix.Rotation(-rotation, 3, 'Z') @ Vector((-offset[0], -offset[1], 1))
|
|
||||||
new_offset[0] /= scale[0]; new_offset[1] /= scale[1]
|
|
||||||
return {
|
|
||||||
"offset": new_offset[0:2],
|
|
||||||
"rotation": -rotation,
|
|
||||||
"scale": [1/scale[0], 1/scale[1]],
|
|
||||||
}
|
|
||||||
|
|
||||||
mapping_transform = inverted(mapping_transform)
|
|
||||||
if mapping_transform is None:
|
|
||||||
gltf2_io_debug.print_console("WARNING",
|
|
||||||
"Skipping exporting texture transform with type TEXTURE because "
|
|
||||||
"we couldn't convert it to TRS; recommend using POINT instead"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
elif mapping_node.vector_type == "VECTOR":
|
|
||||||
# Vectors don't get translated
|
|
||||||
mapping_transform["offset"] = [0, 0]
|
|
||||||
|
|
||||||
texture_transform = texture_transform_blender_to_gltf(mapping_transform)
|
|
||||||
|
|
||||||
if all([component == 0 for component in texture_transform["offset"]]):
|
|
||||||
del(texture_transform["offset"])
|
|
||||||
if all([component == 1 for component in texture_transform["scale"]]):
|
|
||||||
del(texture_transform["scale"])
|
|
||||||
if texture_transform["rotation"] == 0:
|
|
||||||
del(texture_transform["rotation"])
|
|
||||||
|
|
||||||
if len(texture_transform) == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return texture_transform
|
|
||||||
|
|
||||||
|
|
||||||
def get_node(data_path):
|
|
||||||
"""Return Blender node on a given Blender data path."""
|
|
||||||
if data_path is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
index = data_path.find("[\"")
|
|
||||||
if (index == -1):
|
|
||||||
return None
|
|
||||||
|
|
||||||
node_name = data_path[(index + 2):]
|
|
||||||
|
|
||||||
index = node_name.find("\"")
|
|
||||||
if (index == -1):
|
|
||||||
return None
|
|
||||||
|
|
||||||
return node_name[:(index)]
|
|
||||||
|
|
||||||
|
|
||||||
def get_factor_from_socket(socket, kind):
|
|
||||||
"""
|
|
||||||
For baseColorFactor, metallicFactor, etc.
|
|
||||||
Get a constant value from a socket, or a constant value
|
|
||||||
from a MULTIPLY node just before the socket.
|
|
||||||
kind is either 'RGB' or 'VALUE'.
|
|
||||||
"""
|
|
||||||
fac = get_const_from_socket(socket, kind)
|
|
||||||
if fac is not None:
|
|
||||||
return fac
|
|
||||||
|
|
||||||
node = previous_node(socket)
|
|
||||||
if node is not None:
|
|
||||||
x1, x2 = None, None
|
|
||||||
if kind == 'RGB':
|
|
||||||
if node.type == 'MIX' and node.data_type == "RGBA" and node.blend_type == 'MULTIPLY':
|
|
||||||
# TODO: handle factor in inputs[0]?
|
|
||||||
x1 = get_const_from_socket(node.inputs[6], kind)
|
|
||||||
x2 = get_const_from_socket(node.inputs[7], kind)
|
|
||||||
if kind == 'VALUE':
|
|
||||||
if node.type == 'MATH' and node.operation == 'MULTIPLY':
|
|
||||||
x1 = get_const_from_socket(node.inputs[0], kind)
|
|
||||||
x2 = get_const_from_socket(node.inputs[1], kind)
|
|
||||||
if x1 is not None and x2 is None: return x1
|
|
||||||
if x2 is not None and x1 is None: return x2
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_const_from_default_value_socket(socket, kind):
|
|
||||||
if kind == 'RGB':
|
|
||||||
if socket.type != 'RGBA': return None
|
|
||||||
return list(socket.default_value)[:3]
|
|
||||||
if kind == 'VALUE':
|
|
||||||
if socket.type != 'VALUE': return None
|
|
||||||
return socket.default_value
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_const_from_socket(socket, kind):
|
|
||||||
if not socket.is_linked:
|
|
||||||
if kind == 'RGB':
|
|
||||||
if socket.type != 'RGBA': return None
|
|
||||||
return list(socket.default_value)[:3]
|
|
||||||
if kind == 'VALUE':
|
|
||||||
if socket.type != 'VALUE': return None
|
|
||||||
return socket.default_value
|
|
||||||
|
|
||||||
# Handle connection to a constant RGB/Value node
|
|
||||||
prev_node = previous_node(socket)
|
|
||||||
if prev_node is not None:
|
|
||||||
if kind == 'RGB' and prev_node.type == 'RGB':
|
|
||||||
return list(prev_node.outputs[0].default_value)[:3]
|
|
||||||
if kind == 'VALUE' and prev_node.type == 'VALUE':
|
|
||||||
return prev_node.outputs[0].default_value
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def previous_socket(socket):
|
|
||||||
while True:
|
|
||||||
if not socket.is_linked:
|
|
||||||
return None
|
|
||||||
|
|
||||||
from_socket = socket.links[0].from_socket
|
|
||||||
|
|
||||||
# Skip over reroute nodes
|
|
||||||
if from_socket.node.type == 'REROUTE':
|
|
||||||
socket = from_socket.node.inputs[0]
|
|
||||||
continue
|
|
||||||
|
|
||||||
return from_socket
|
|
||||||
|
|
||||||
|
|
||||||
def previous_node(socket):
|
|
||||||
prev_socket = previous_socket(socket)
|
|
||||||
if prev_socket is not None:
|
|
||||||
return prev_socket.node
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_tex_from_socket(socket):
|
|
||||||
result = gltf2_blender_search_node_tree.from_socket(
|
|
||||||
socket,
|
|
||||||
gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
|
|
||||||
if not result:
|
|
||||||
return None
|
|
||||||
if result[0].shader_node.image is None:
|
|
||||||
return None
|
|
||||||
return result[0]
|
|
||||||
|
|
||||||
def has_image_node_from_socket(socket):
|
|
||||||
return get_tex_from_socket(socket) is not None
|
|
||||||
|
|
||||||
def image_tex_is_valid_from_socket(socket):
|
|
||||||
res = get_tex_from_socket(socket)
|
|
||||||
return res is not None and res.shader_node.image is not None and res.shader_node.image.channels != 0
|
|
||||||
|
@ -117,7 +117,7 @@ class GlTF2Exporter:
|
|||||||
f.write(self.__buffer.to_bytes())
|
f.write(self.__buffer.to_bytes())
|
||||||
uri = buffer_name
|
uri = buffer_name
|
||||||
else:
|
else:
|
||||||
uri = self.__buffer.to_embed_string()
|
pass # This is no more possible, we don't export embedded buffers
|
||||||
|
|
||||||
buffer = gltf2_io.Buffer(
|
buffer = gltf2_io.Buffer(
|
||||||
byte_length=self.__buffer.byte_length,
|
byte_length=self.__buffer.byte_length,
|
||||||
@ -320,6 +320,20 @@ class GlTF2Exporter:
|
|||||||
len_ = len([i for i in self.nodes_idx_to_remove if i < skin.skeleton])
|
len_ = len([i for i in self.nodes_idx_to_remove if i < skin.skeleton])
|
||||||
skin.skeleton = skin.skeleton - len_
|
skin.skeleton = skin.skeleton - len_
|
||||||
|
|
||||||
|
# Remove animation channels that was targeting a node that will be removed
|
||||||
|
new_animation_list = []
|
||||||
|
for animation in self.__gltf.animations:
|
||||||
|
new_channel_list = []
|
||||||
|
for channel in animation.channels:
|
||||||
|
if channel.target.node not in self.nodes_idx_to_remove:
|
||||||
|
new_channel_list.append(channel)
|
||||||
|
animation.channels = new_channel_list
|
||||||
|
if len(animation.channels) > 0:
|
||||||
|
new_animation_list.append(animation)
|
||||||
|
self.__gltf.animations = new_animation_list
|
||||||
|
|
||||||
|
#TODO: remove unused animation accessors?
|
||||||
|
|
||||||
# And now really remove nodes
|
# 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]
|
self.__gltf.nodes = [node for idx, node in enumerate(self.__gltf.nodes) if idx not in self.nodes_idx_to_remove]
|
||||||
|
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from .....io.com.gltf2_io_extensions import Extension
|
from .....io.com.gltf2_io_extensions import Extension
|
||||||
from ....exp import gltf2_blender_get
|
|
||||||
from ...material import gltf2_blender_gather_texture_info
|
from ...material import gltf2_blender_gather_texture_info
|
||||||
|
from ..gltf2_blender_search_node_tree import has_image_node_from_socket, get_socket, get_factor_from_socket
|
||||||
|
|
||||||
def export_clearcoat(blender_material, export_settings):
|
def export_clearcoat(blender_material, export_settings):
|
||||||
clearcoat_enabled = False
|
clearcoat_enabled = False
|
||||||
@ -15,15 +15,15 @@ def export_clearcoat(blender_material, export_settings):
|
|||||||
clearcoat_extension = {}
|
clearcoat_extension = {}
|
||||||
clearcoat_roughness_slots = ()
|
clearcoat_roughness_slots = ()
|
||||||
|
|
||||||
clearcoat_socket = gltf2_blender_get.get_socket(blender_material, 'Coat Weight')
|
clearcoat_socket = get_socket(blender_material, 'Coat Weight')
|
||||||
clearcoat_roughness_socket = gltf2_blender_get.get_socket(blender_material, 'Coat Roughness')
|
clearcoat_roughness_socket = get_socket(blender_material, 'Coat Roughness')
|
||||||
clearcoat_normal_socket = gltf2_blender_get.get_socket(blender_material, 'Coat Normal')
|
clearcoat_normal_socket = get_socket(blender_material, 'Coat Normal')
|
||||||
|
|
||||||
if isinstance(clearcoat_socket, bpy.types.NodeSocket) and not clearcoat_socket.is_linked:
|
if isinstance(clearcoat_socket.socket, bpy.types.NodeSocket) and not clearcoat_socket.socket.is_linked:
|
||||||
clearcoat_extension['clearcoatFactor'] = clearcoat_socket.default_value
|
clearcoat_extension['clearcoatFactor'] = clearcoat_socket.socket.default_value
|
||||||
clearcoat_enabled = clearcoat_extension['clearcoatFactor'] > 0
|
clearcoat_enabled = clearcoat_extension['clearcoatFactor'] > 0
|
||||||
elif gltf2_blender_get.has_image_node_from_socket(clearcoat_socket):
|
elif has_image_node_from_socket(clearcoat_socket, export_settings):
|
||||||
fac = gltf2_blender_get.get_factor_from_socket(clearcoat_socket, kind='VALUE')
|
fac = get_factor_from_socket(clearcoat_socket, kind='VALUE')
|
||||||
# default value in glTF is 0.0, but if there is a texture without factor, use 1
|
# default value in glTF is 0.0, but if there is a texture without factor, use 1
|
||||||
clearcoat_extension['clearcoatFactor'] = fac if fac != None else 1.0
|
clearcoat_extension['clearcoatFactor'] = fac if fac != None else 1.0
|
||||||
has_clearcoat_texture = True
|
has_clearcoat_texture = True
|
||||||
@ -32,10 +32,10 @@ def export_clearcoat(blender_material, export_settings):
|
|||||||
if not clearcoat_enabled:
|
if not clearcoat_enabled:
|
||||||
return None, {}
|
return None, {}
|
||||||
|
|
||||||
if isinstance(clearcoat_roughness_socket, bpy.types.NodeSocket) and not clearcoat_roughness_socket.is_linked:
|
if isinstance(clearcoat_roughness_socket.socket, bpy.types.NodeSocket) and not clearcoat_roughness_socket.socket.is_linked:
|
||||||
clearcoat_extension['clearcoatRoughnessFactor'] = clearcoat_roughness_socket.default_value
|
clearcoat_extension['clearcoatRoughnessFactor'] = clearcoat_roughness_socket.socket.default_value
|
||||||
elif gltf2_blender_get.has_image_node_from_socket(clearcoat_roughness_socket):
|
elif has_image_node_from_socket(clearcoat_roughness_socket, export_settings):
|
||||||
fac = gltf2_blender_get.get_factor_from_socket(clearcoat_roughness_socket, kind='VALUE')
|
fac = get_factor_from_socket(clearcoat_roughness_socket, kind='VALUE')
|
||||||
# default value in glTF is 0.0, but if there is a texture without factor, use 1
|
# default value in glTF is 0.0, but if there is a texture without factor, use 1
|
||||||
clearcoat_extension['clearcoatRoughnessFactor'] = fac if fac != None else 1.0
|
clearcoat_extension['clearcoatRoughnessFactor'] = fac if fac != None else 1.0
|
||||||
has_clearcoat_roughness_texture = True
|
has_clearcoat_roughness_texture = True
|
||||||
@ -71,7 +71,7 @@ def export_clearcoat(blender_material, export_settings):
|
|||||||
clearcoat_extension['clearcoatRoughnessTexture'] = clearcoat_roughness_texture
|
clearcoat_extension['clearcoatRoughnessTexture'] = clearcoat_roughness_texture
|
||||||
uvmap_infos.update({'clearcoatRoughnessTexture': uvmap_info})
|
uvmap_infos.update({'clearcoatRoughnessTexture': uvmap_info})
|
||||||
|
|
||||||
if gltf2_blender_get.has_image_node_from_socket(clearcoat_normal_socket):
|
if has_image_node_from_socket(clearcoat_normal_socket, export_settings):
|
||||||
clearcoat_normal_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class(
|
clearcoat_normal_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class(
|
||||||
clearcoat_normal_socket,
|
clearcoat_normal_socket,
|
||||||
(clearcoat_normal_socket,),
|
(clearcoat_normal_socket,),
|
||||||
|
@ -4,20 +4,26 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from .....io.com.gltf2_io_extensions import Extension
|
from .....io.com.gltf2_io_extensions import Extension
|
||||||
from ....exp import gltf2_blender_get
|
|
||||||
from ...material import gltf2_blender_gather_texture_info
|
from ...material import gltf2_blender_gather_texture_info
|
||||||
|
from ..gltf2_blender_search_node_tree import \
|
||||||
|
get_const_from_default_value_socket, \
|
||||||
|
get_socket, \
|
||||||
|
get_factor_from_socket, \
|
||||||
|
get_const_from_socket, \
|
||||||
|
NodeSocket, \
|
||||||
|
get_socket_from_gltf_material_node
|
||||||
|
|
||||||
def export_emission_factor(blender_material, export_settings):
|
def export_emission_factor(blender_material, export_settings):
|
||||||
emissive_socket = gltf2_blender_get.get_socket(blender_material, "Emissive")
|
emissive_socket = get_socket(blender_material, "Emissive")
|
||||||
if emissive_socket is None:
|
if emissive_socket.socket is None:
|
||||||
emissive_socket = gltf2_blender_get.get_socket_old(blender_material, "EmissiveFactor")
|
emissive_socket = get_socket_from_gltf_material_node(blender_material, "EmissiveFactor")
|
||||||
if isinstance(emissive_socket, bpy.types.NodeSocket):
|
if isinstance(emissive_socket.socket, bpy.types.NodeSocket):
|
||||||
if export_settings['gltf_image_format'] != "NONE":
|
if export_settings['gltf_image_format'] != "NONE":
|
||||||
factor = gltf2_blender_get.get_factor_from_socket(emissive_socket, kind='RGB')
|
factor = get_factor_from_socket(emissive_socket, kind='RGB')
|
||||||
else:
|
else:
|
||||||
factor = gltf2_blender_get.get_const_from_default_value_socket(emissive_socket, kind='RGB')
|
factor = get_const_from_default_value_socket(emissive_socket, kind='RGB')
|
||||||
|
|
||||||
if factor is None and emissive_socket.is_linked:
|
if factor is None and emissive_socket.socket.is_linked:
|
||||||
# In glTF, the default emissiveFactor is all zeros, so if an emission texture is connected,
|
# In glTF, the default emissiveFactor is all zeros, so if an emission texture is connected,
|
||||||
# we have to manually set it to all ones.
|
# we have to manually set it to all ones.
|
||||||
factor = [1.0, 1.0, 1.0]
|
factor = [1.0, 1.0, 1.0]
|
||||||
@ -26,12 +32,12 @@ def export_emission_factor(blender_material, export_settings):
|
|||||||
|
|
||||||
# Handle Emission Strength
|
# Handle Emission Strength
|
||||||
strength_socket = None
|
strength_socket = None
|
||||||
if emissive_socket.node.type == 'EMISSION':
|
if emissive_socket.socket.node.type == 'EMISSION':
|
||||||
strength_socket = emissive_socket.node.inputs['Strength']
|
strength_socket = emissive_socket.socket.node.inputs['Strength']
|
||||||
elif 'Emission Strength' in emissive_socket.node.inputs:
|
elif 'Emission Strength' in emissive_socket.socket.node.inputs:
|
||||||
strength_socket = emissive_socket.node.inputs['Emission Strength']
|
strength_socket = emissive_socket.socket.node.inputs['Emission Strength']
|
||||||
strength = (
|
strength = (
|
||||||
gltf2_blender_get.get_const_from_socket(strength_socket, kind='VALUE')
|
get_const_from_socket(NodeSocket(strength_socket, emissive_socket.group_path), kind='VALUE')
|
||||||
if strength_socket is not None
|
if strength_socket is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
@ -49,9 +55,9 @@ def export_emission_factor(blender_material, export_settings):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def export_emission_texture(blender_material, export_settings):
|
def export_emission_texture(blender_material, export_settings):
|
||||||
emissive = gltf2_blender_get.get_socket(blender_material, "Emissive")
|
emissive = get_socket(blender_material, "Emissive")
|
||||||
if emissive is None:
|
if emissive.socket is None:
|
||||||
emissive = gltf2_blender_get.get_socket_old(blender_material, "Emissive")
|
emissive = get_socket_from_gltf_material_node(blender_material, "Emissive")
|
||||||
emissive_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_texture_info(emissive, (emissive,), (), export_settings)
|
emissive_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_texture_info(emissive, (emissive,), (), export_settings)
|
||||||
return emissive_texture, {'emissiveTexture': uvmap_info}
|
return emissive_texture, {'emissiveTexture': uvmap_info}
|
||||||
|
|
||||||
|
@ -4,20 +4,20 @@
|
|||||||
|
|
||||||
from .....io.com.gltf2_io_extensions import Extension
|
from .....io.com.gltf2_io_extensions import Extension
|
||||||
from .....io.com.gltf2_io_constants import GLTF_IOR
|
from .....io.com.gltf2_io_constants import GLTF_IOR
|
||||||
from ....exp import gltf2_blender_get
|
from ..gltf2_blender_search_node_tree import get_socket
|
||||||
|
|
||||||
def export_ior(blender_material, extensions, export_settings):
|
def export_ior(blender_material, extensions, export_settings):
|
||||||
ior_socket = gltf2_blender_get.get_socket(blender_material, 'IOR')
|
ior_socket = get_socket(blender_material, 'IOR')
|
||||||
|
|
||||||
if not ior_socket:
|
if not ior_socket.socket:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# We don't manage case where socket is linked, always check default value
|
# We don't manage case where socket is linked, always check default value
|
||||||
if ior_socket.is_linked:
|
if ior_socket.socket.is_linked:
|
||||||
# TODOExt: add warning?
|
# TODOExt: add warning?
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if ior_socket.default_value == GLTF_IOR:
|
if ior_socket.socket.default_value == GLTF_IOR:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Export only if the following extensions are exported:
|
# Export only if the following extensions are exported:
|
||||||
@ -31,6 +31,6 @@ def export_ior(blender_material, extensions, export_settings):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
ior_extension = {}
|
ior_extension = {}
|
||||||
ior_extension['ior'] = ior_socket.default_value
|
ior_extension['ior'] = ior_socket.socket.default_value
|
||||||
|
|
||||||
return Extension('KHR_materials_ior', ior_extension, False)
|
return Extension('KHR_materials_ior', ior_extension, False)
|
||||||
|
@ -4,47 +4,48 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from .....io.com.gltf2_io_extensions import Extension
|
from .....io.com.gltf2_io_extensions import Extension
|
||||||
from ....exp import gltf2_blender_get
|
|
||||||
from ...material import gltf2_blender_gather_texture_info
|
from ...material import gltf2_blender_gather_texture_info
|
||||||
|
from ..gltf2_blender_search_node_tree import \
|
||||||
|
has_image_node_from_socket, \
|
||||||
|
get_socket, \
|
||||||
|
get_factor_from_socket
|
||||||
|
|
||||||
|
|
||||||
def export_sheen(blender_material, export_settings):
|
def export_sheen(blender_material, export_settings):
|
||||||
sheen_extension = {}
|
sheen_extension = {}
|
||||||
|
|
||||||
sheenTint_socket = gltf2_blender_get.get_socket(blender_material, "Sheen Tint")
|
sheenTint_socket = get_socket(blender_material, "Sheen Tint")
|
||||||
sheenRoughness_socket = gltf2_blender_get.get_socket(blender_material, "Sheen Roughness")
|
sheenRoughness_socket = get_socket(blender_material, "Sheen Roughness")
|
||||||
sheen_socket = gltf2_blender_get.get_socket(blender_material, "Sheen Weight")
|
sheen_socket = get_socket(blender_material, "Sheen Weight")
|
||||||
|
|
||||||
if sheenTint_socket is None or sheenRoughness_socket is None or sheen_socket is None:
|
if sheenTint_socket.socket is None or sheenRoughness_socket.socket is None or sheen_socket.socket is None:
|
||||||
return None, {}
|
return None, {}
|
||||||
|
|
||||||
if sheen_socket.is_linked is False and sheen_socket.default_value == 0.0:
|
if sheen_socket.socket.is_linked is False and sheen_socket.socket.default_value == 0.0:
|
||||||
return None, {}
|
return None, {}
|
||||||
|
|
||||||
uvmap_infos = {}
|
uvmap_infos = {}
|
||||||
|
|
||||||
#TODOExt : What to do if sheen_socket is linked? or is not between 0 and 1?
|
#TODOExt : What to do if sheen_socket is linked? or is not between 0 and 1?
|
||||||
|
|
||||||
sheenTint_non_linked = isinstance(sheenTint_socket, bpy.types.NodeSocket) and not sheenTint_socket.is_linked
|
sheenTint_non_linked = isinstance(sheenTint_socket.socket, bpy.types.NodeSocket) and not sheenTint_socket.socket.is_linked
|
||||||
sheenRoughness_non_linked = isinstance(sheenRoughness_socket, bpy.types.NodeSocket) and not sheenRoughness_socket.is_linked
|
sheenRoughness_non_linked = isinstance(sheenRoughness_socket.socket, bpy.types.NodeSocket) and not sheenRoughness_socket.socket.is_linked
|
||||||
|
|
||||||
|
|
||||||
use_actives_uvmaps = []
|
|
||||||
|
|
||||||
if sheenTint_non_linked is True:
|
if sheenTint_non_linked is True:
|
||||||
color = sheenTint_socket.default_value[:3]
|
color = sheenTint_socket.socket.default_value[:3]
|
||||||
if color != (0.0, 0.0, 0.0):
|
if color != (0.0, 0.0, 0.0):
|
||||||
sheen_extension['sheenColorFactor'] = color
|
sheen_extension['sheenColorFactor'] = color
|
||||||
else:
|
else:
|
||||||
# Factor
|
# Factor
|
||||||
fac = gltf2_blender_get.get_factor_from_socket(sheenTint_socket, kind='RGB')
|
fac = get_factor_from_socket(sheenTint_socket, kind='RGB')
|
||||||
if fac is None:
|
if fac is None:
|
||||||
fac = [1.0, 1.0, 1.0] # Default is 0.0/0.0/0.0, so we need to set it to 1 if no factor
|
fac = [1.0, 1.0, 1.0] # Default is 0.0/0.0/0.0, so we need to set it to 1 if no factor
|
||||||
if fac is not None and fac != [0.0, 0.0, 0.0]:
|
if fac is not None and fac != [0.0, 0.0, 0.0]:
|
||||||
sheen_extension['sheenColorFactor'] = fac
|
sheen_extension['sheenColorFactor'] = fac
|
||||||
|
|
||||||
# Texture
|
# Texture
|
||||||
if gltf2_blender_get.has_image_node_from_socket(sheenTint_socket):
|
if has_image_node_from_socket(sheenTint_socket, export_settings):
|
||||||
original_sheenColor_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_texture_info(
|
original_sheenColor_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_texture_info(
|
||||||
sheenTint_socket,
|
sheenTint_socket,
|
||||||
(sheenTint_socket,),
|
(sheenTint_socket,),
|
||||||
@ -55,19 +56,19 @@ def export_sheen(blender_material, export_settings):
|
|||||||
uvmap_infos.update({'sheenColorTexture': uvmap_info})
|
uvmap_infos.update({'sheenColorTexture': uvmap_info})
|
||||||
|
|
||||||
if sheenRoughness_non_linked is True:
|
if sheenRoughness_non_linked is True:
|
||||||
fac = sheenRoughness_socket.default_value
|
fac = sheenRoughness_socket.socket.default_value
|
||||||
if fac != 0.0:
|
if fac != 0.0:
|
||||||
sheen_extension['sheenRoughnessFactor'] = fac
|
sheen_extension['sheenRoughnessFactor'] = fac
|
||||||
else:
|
else:
|
||||||
# Factor
|
# Factor
|
||||||
fac = gltf2_blender_get.get_factor_from_socket(sheenRoughness_socket, kind='VALUE')
|
fac = get_factor_from_socket(sheenRoughness_socket, kind='VALUE')
|
||||||
if fac is None:
|
if fac is None:
|
||||||
fac = 1.0 # Default is 0.0 so we need to set it to 1.0 if no factor
|
fac = 1.0 # Default is 0.0 so we need to set it to 1.0 if no factor
|
||||||
if fac is not None and fac != 0.0:
|
if fac is not None and fac != 0.0:
|
||||||
sheen_extension['sheenRoughnessFactor'] = fac
|
sheen_extension['sheenRoughnessFactor'] = fac
|
||||||
|
|
||||||
# Texture
|
# Texture
|
||||||
if gltf2_blender_get.has_image_node_from_socket(sheenRoughness_socket):
|
if has_image_node_from_socket(sheenRoughness_socket, export_settings):
|
||||||
original_sheenRoughness_texture, uvmap_info , _ = gltf2_blender_gather_texture_info.gather_texture_info(
|
original_sheenRoughness_texture, uvmap_info , _ = gltf2_blender_gather_texture_info.gather_texture_info(
|
||||||
sheenRoughness_socket,
|
sheenRoughness_socket,
|
||||||
(sheenRoughness_socket,),
|
(sheenRoughness_socket,),
|
||||||
|
@ -4,68 +4,96 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from .....io.com.gltf2_io_extensions import Extension
|
from .....io.com.gltf2_io_extensions import Extension
|
||||||
from ....exp import gltf2_blender_get
|
|
||||||
from ...material.gltf2_blender_gather_texture_info import gather_texture_info
|
from ...material.gltf2_blender_gather_texture_info import gather_texture_info
|
||||||
|
from ..gltf2_blender_search_node_tree import \
|
||||||
|
has_image_node_from_socket, \
|
||||||
|
get_socket, \
|
||||||
|
get_factor_from_socket
|
||||||
|
|
||||||
|
|
||||||
def export_specular(blender_material, export_settings):
|
def export_specular(blender_material, export_settings):
|
||||||
specular_extension = {}
|
specular_extension = {}
|
||||||
|
extensions_needed = False
|
||||||
|
|
||||||
specular_socket = gltf2_blender_get.get_socket(blender_material, 'Specular IOR Level')
|
specular_socket = get_socket(blender_material, 'Specular IOR Level')
|
||||||
speculartint_socket = gltf2_blender_get.get_socket(blender_material, 'Specular Tint')
|
speculartint_socket = get_socket(blender_material, 'Specular Tint')
|
||||||
|
|
||||||
if specular_socket is None or speculartint_socket is None:
|
if specular_socket.socket is None or speculartint_socket.socket is None:
|
||||||
return None, {}
|
return None, {}
|
||||||
|
|
||||||
uvmap_infos = {}
|
uvmap_infos = {}
|
||||||
|
|
||||||
specular_non_linked = isinstance(specular_socket, bpy.types.NodeSocket) and not specular_socket.is_linked
|
specular_non_linked = isinstance(specular_socket.socket, bpy.types.NodeSocket) and not specular_socket.socket.is_linked
|
||||||
specularcolor_non_linked = isinstance(speculartint_socket, bpy.types.NodeSocket) and not speculartint_socket.is_linked
|
specularcolor_non_linked = isinstance(speculartint_socket.socket, bpy.types.NodeSocket) and not speculartint_socket.socket.is_linked
|
||||||
|
|
||||||
if specular_non_linked is True:
|
if specular_non_linked is True:
|
||||||
fac = specular_socket.default_value
|
fac = specular_socket.socket.default_value
|
||||||
if fac != 1.0:
|
fac = fac * 2.0
|
||||||
|
if fac < 1.0:
|
||||||
specular_extension['specularFactor'] = fac
|
specular_extension['specularFactor'] = fac
|
||||||
if fac == 0.0:
|
extensions_needed = True
|
||||||
return None, {}
|
elif fac > 1.0:
|
||||||
|
# glTF specularFactor should be <= 1.0, so we will multiply ColorFactory by specularFactor, and set SpecularFactor to 1.0 (default value)
|
||||||
|
extensions_needed = True
|
||||||
|
else:
|
||||||
|
pass # If fac == 1.0, no need to export specularFactor, the default value is 1.0
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Factor
|
# Factor
|
||||||
fac = gltf2_blender_get.get_factor_from_socket(specular_socket, kind='VALUE')
|
fac = get_factor_from_socket(specular_socket, kind='VALUE')
|
||||||
if fac is not None and fac != 1.0:
|
if fac is not None and fac != 1.0:
|
||||||
specular_extension['specularFactor'] = fac
|
fac = fac * 2.0 if fac is not None else None
|
||||||
|
if fac is not None and fac < 1.0:
|
||||||
if fac == 0.0:
|
specular_extension['specularFactor'] = fac
|
||||||
return None, {}
|
extensions_needed = True
|
||||||
|
elif fac is not None and fac > 1.0:
|
||||||
|
# glTF specularFactor should be <= 1.0, so we will multiply ColorFactory by specularFactor, and set SpecularFactor to 1.0 (default value)
|
||||||
|
extensions_needed = True
|
||||||
|
|
||||||
# Texture
|
# Texture
|
||||||
if gltf2_blender_get.has_image_node_from_socket(specular_socket):
|
if has_image_node_from_socket(specular_socket, export_settings):
|
||||||
original_specular_texture, uvmap_info, _ = gather_texture_info(
|
specular_texture, uvmap_info, _ = gather_texture_info(
|
||||||
specular_socket,
|
specular_socket,
|
||||||
(specular_socket,),
|
(specular_socket,),
|
||||||
(),
|
(),
|
||||||
export_settings,
|
export_settings,
|
||||||
)
|
)
|
||||||
specular_extension['specularTexture'] = original_specular_texture
|
specular_extension['specularTexture'] = specular_texture
|
||||||
uvmap_infos.update({'specularTexture': uvmap_info})
|
uvmap_infos.update({'specularTexture': uvmap_info})
|
||||||
|
extensions_needed = True
|
||||||
|
|
||||||
if specularcolor_non_linked is True:
|
if specularcolor_non_linked is True:
|
||||||
color = speculartint_socket.default_value[:3]
|
color = speculartint_socket.socket.default_value[:3]
|
||||||
|
if fac is not None and fac > 1.0:
|
||||||
|
color = (color[0] * fac, color[1] * fac, color[2] * fac)
|
||||||
|
specular_extension['specularColorFactor'] = color if color != (1.0, 1.0, 1.0) else None
|
||||||
if color != (1.0, 1.0, 1.0):
|
if color != (1.0, 1.0, 1.0):
|
||||||
specular_extension['specularColorFactor'] = color
|
extensions_needed = True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Factor
|
# Factor
|
||||||
fac = gltf2_blender_get.get_factor_from_socket(speculartint_socket, kind='RGB')
|
fac_color = get_factor_from_socket(speculartint_socket, kind='RGB')
|
||||||
if fac is not None and fac != (1.0, 1.0, 1.0):
|
if fac_color is not None and fac is not None and fac > 1.0:
|
||||||
specular_extension['specularColorFactor'] = fac
|
fac_color = (fac_color[0] * fac, fac_color[1] * fac, fac_color[2] * fac)
|
||||||
|
elif fac_color is None and fac is not None and fac > 1.0:
|
||||||
|
fac_color = (fac, fac, fac)
|
||||||
|
specular_extension['specularColorFactor'] = fac_color if fac_color != (1.0, 1.0, 1.0) else None
|
||||||
|
if fac_color != (1.0, 1.0, 1.0):
|
||||||
|
extensions_needed = True
|
||||||
|
|
||||||
# Texture
|
# Texture
|
||||||
if gltf2_blender_get.has_image_node_from_socket(speculartint_socket):
|
if has_image_node_from_socket(speculartint_socket, export_settings):
|
||||||
original_specularcolor_texture, uvmap_info, _ = gather_texture_info(
|
specularcolor_texture, uvmap_info, _ = gather_texture_info(
|
||||||
speculartint_socket,
|
speculartint_socket,
|
||||||
(speculartint_socket,),
|
(speculartint_socket,),
|
||||||
(),
|
(),
|
||||||
export_settings,
|
export_settings,
|
||||||
)
|
)
|
||||||
specular_extension['specularColorTexture'] = original_specularcolor_texture
|
specular_extension['specularColorTexture'] = specularcolor_texture
|
||||||
uvmap_infos.update({'specularColorTexture': uvmap_info})
|
uvmap_infos.update({'specularColorTexture': uvmap_info})
|
||||||
|
extensions_needed = True
|
||||||
|
|
||||||
|
if extensions_needed is False:
|
||||||
|
return None, {}
|
||||||
|
|
||||||
return Extension('KHR_materials_specular', specular_extension, False), uvmap_infos
|
return Extension('KHR_materials_specular', specular_extension, False), uvmap_infos
|
||||||
|
@ -4,8 +4,11 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from .....io.com.gltf2_io_extensions import Extension
|
from .....io.com.gltf2_io_extensions import Extension
|
||||||
from ....exp import gltf2_blender_get
|
|
||||||
from ...material import gltf2_blender_gather_texture_info
|
from ...material import gltf2_blender_gather_texture_info
|
||||||
|
from ..gltf2_blender_search_node_tree import \
|
||||||
|
has_image_node_from_socket, \
|
||||||
|
get_socket, \
|
||||||
|
get_factor_from_socket
|
||||||
|
|
||||||
def export_transmission(blender_material, export_settings):
|
def export_transmission(blender_material, export_settings):
|
||||||
transmission_enabled = False
|
transmission_enabled = False
|
||||||
@ -14,13 +17,13 @@ def export_transmission(blender_material, export_settings):
|
|||||||
transmission_extension = {}
|
transmission_extension = {}
|
||||||
transmission_slots = ()
|
transmission_slots = ()
|
||||||
|
|
||||||
transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission Weight')
|
transmission_socket = get_socket(blender_material, 'Transmission Weight')
|
||||||
|
|
||||||
if isinstance(transmission_socket, bpy.types.NodeSocket) and not transmission_socket.is_linked:
|
if isinstance(transmission_socket.socket, bpy.types.NodeSocket) and not transmission_socket.socket.is_linked:
|
||||||
transmission_extension['transmissionFactor'] = transmission_socket.default_value
|
transmission_extension['transmissionFactor'] = transmission_socket.socket.default_value
|
||||||
transmission_enabled = transmission_extension['transmissionFactor'] > 0
|
transmission_enabled = transmission_extension['transmissionFactor'] > 0
|
||||||
elif gltf2_blender_get.has_image_node_from_socket(transmission_socket):
|
elif has_image_node_from_socket(transmission_socket, export_settings):
|
||||||
fac = gltf2_blender_get.get_factor_from_socket(transmission_socket, kind='VALUE')
|
fac = get_factor_from_socket(transmission_socket, kind='VALUE')
|
||||||
transmission_extension['transmissionFactor'] = fac if fac is not None else 1.0
|
transmission_extension['transmissionFactor'] = fac if fac is not None else 1.0
|
||||||
has_transmission_texture = True
|
has_transmission_texture = True
|
||||||
transmission_enabled = True
|
transmission_enabled = True
|
||||||
|
@ -4,8 +4,13 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from .....io.com.gltf2_io_extensions import Extension
|
from .....io.com.gltf2_io_extensions import Extension
|
||||||
from ....exp import gltf2_blender_get
|
|
||||||
from ...material import gltf2_blender_gather_texture_info
|
from ...material import gltf2_blender_gather_texture_info
|
||||||
|
from ..gltf2_blender_search_node_tree import \
|
||||||
|
has_image_node_from_socket, \
|
||||||
|
get_const_from_default_value_socket, \
|
||||||
|
get_socket_from_gltf_material_node, \
|
||||||
|
get_socket, \
|
||||||
|
get_factor_from_socket
|
||||||
|
|
||||||
|
|
||||||
def export_volume(blender_material, export_settings):
|
def export_volume(blender_material, export_settings):
|
||||||
@ -13,10 +18,10 @@ def export_volume(blender_material, export_settings):
|
|||||||
|
|
||||||
# If no transmission --> No volume
|
# If no transmission --> No volume
|
||||||
transmission_enabled = False
|
transmission_enabled = False
|
||||||
transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission Weight')
|
transmission_socket = get_socket(blender_material, 'Transmission Weight')
|
||||||
if isinstance(transmission_socket, bpy.types.NodeSocket) and not transmission_socket.is_linked:
|
if isinstance(transmission_socket.socket, bpy.types.NodeSocket) and not transmission_socket.socket.is_linked:
|
||||||
transmission_enabled = transmission_socket.default_value > 0
|
transmission_enabled = transmission_socket.socket.default_value > 0
|
||||||
elif gltf2_blender_get.has_image_node_from_socket(transmission_socket):
|
elif has_image_node_from_socket(transmission_socket, export_settings):
|
||||||
transmission_enabled = True
|
transmission_enabled = True
|
||||||
|
|
||||||
if transmission_enabled is False:
|
if transmission_enabled is False:
|
||||||
@ -27,43 +32,43 @@ def export_volume(blender_material, export_settings):
|
|||||||
thickness_slots = ()
|
thickness_slots = ()
|
||||||
uvmap_info = {}
|
uvmap_info = {}
|
||||||
|
|
||||||
thicknesss_socket = gltf2_blender_get.get_socket_old(blender_material, 'Thickness')
|
thickness_socket = get_socket_from_gltf_material_node(blender_material, 'Thickness')
|
||||||
if thicknesss_socket is None:
|
if thickness_socket.socket is None:
|
||||||
# If no thickness (here because there is no glTF Material Output node), no volume extension export
|
# If no thickness (here because there is no glTF Material Output node), no volume extension export
|
||||||
return None, {}
|
return None, {}
|
||||||
|
|
||||||
density_socket = gltf2_blender_get.get_socket(blender_material, 'Density', volume=True)
|
density_socket = get_socket(blender_material, 'Density', volume=True)
|
||||||
attenuation_color_socket = gltf2_blender_get.get_socket(blender_material, 'Color', volume=True)
|
attenuation_color_socket = get_socket(blender_material, 'Color', volume=True)
|
||||||
# Even if density or attenuation are not set, we export volume extension
|
# Even if density or attenuation are not set, we export volume extension
|
||||||
|
|
||||||
if isinstance(attenuation_color_socket, bpy.types.NodeSocket):
|
if isinstance(attenuation_color_socket.socket, bpy.types.NodeSocket):
|
||||||
rgb = gltf2_blender_get.get_const_from_default_value_socket(attenuation_color_socket, kind='RGB')
|
rgb = get_const_from_default_value_socket(attenuation_color_socket, kind='RGB')
|
||||||
volume_extension['attenuationColor'] = rgb
|
volume_extension['attenuationColor'] = rgb
|
||||||
|
|
||||||
if isinstance(density_socket, bpy.types.NodeSocket):
|
if isinstance(density_socket.socket, bpy.types.NodeSocket):
|
||||||
density = gltf2_blender_get.get_const_from_default_value_socket(density_socket, kind='VALUE')
|
density = get_const_from_default_value_socket(density_socket, kind='VALUE')
|
||||||
volume_extension['attenuationDistance'] = 1.0 / density if density != 0 else None # infinity (Using None as glTF default)
|
volume_extension['attenuationDistance'] = 1.0 / density if density != 0 else None # infinity (Using None as glTF default)
|
||||||
|
|
||||||
|
|
||||||
if isinstance(thicknesss_socket, bpy.types.NodeSocket) and not thicknesss_socket.is_linked:
|
if isinstance(thickness_socket.socket, bpy.types.NodeSocket) and not thickness_socket.socket.is_linked:
|
||||||
val = thicknesss_socket.default_value
|
val = thickness_socket.socket.default_value
|
||||||
if val == 0.0:
|
if val == 0.0:
|
||||||
# If no thickness, no volume extension export
|
# If no thickness, no volume extension export
|
||||||
return None, {}
|
return None, {}
|
||||||
volume_extension['thicknessFactor'] = val
|
volume_extension['thicknessFactor'] = val
|
||||||
elif gltf2_blender_get.has_image_node_from_socket(thicknesss_socket):
|
elif has_image_node_from_socket(thickness_socket, export_settings):
|
||||||
fac = gltf2_blender_get.get_factor_from_socket(thicknesss_socket, kind='VALUE')
|
fac = get_factor_from_socket(thickness_socket, kind='VALUE')
|
||||||
# default value in glTF is 0.0, but if there is a texture without factor, use 1
|
# default value in glTF is 0.0, but if there is a texture without factor, use 1
|
||||||
volume_extension['thicknessFactor'] = fac if fac != None else 1.0
|
volume_extension['thicknessFactor'] = fac if fac != None else 1.0
|
||||||
has_thickness_texture = True
|
has_thickness_texture = True
|
||||||
|
|
||||||
# Pack thickness channel (R).
|
# Pack thickness channel (G).
|
||||||
if has_thickness_texture:
|
if has_thickness_texture:
|
||||||
thickness_slots = (thicknesss_socket,)
|
thickness_slots = (thickness_socket,)
|
||||||
|
|
||||||
if len(thickness_slots) > 0:
|
if len(thickness_slots) > 0:
|
||||||
combined_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_texture_info(
|
combined_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_texture_info(
|
||||||
thicknesss_socket,
|
thickness_socket,
|
||||||
thickness_slots,
|
thickness_slots,
|
||||||
(),
|
(),
|
||||||
export_settings,
|
export_settings,
|
||||||
|
@ -147,7 +147,6 @@ class ExportImage:
|
|||||||
|
|
||||||
# Unhappy path = we need to create the image self.fills describes or self.stores describes
|
# Unhappy path = we need to create the image self.fills describes or self.stores describes
|
||||||
if self.numpy_calc is None:
|
if self.numpy_calc is None:
|
||||||
print(">2")
|
|
||||||
return self.__encode_unhappy(export_settings), None
|
return self.__encode_unhappy(export_settings), None
|
||||||
else:
|
else:
|
||||||
pixels, width, height, factor = self.numpy_calc(self.stored)
|
pixels, width, height, factor = self.numpy_calc(self.stored)
|
||||||
|
@ -13,7 +13,7 @@ from ....io.com import gltf2_io_debug
|
|||||||
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
|
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
|
||||||
from ..gltf2_blender_gather_cache import cached
|
from ..gltf2_blender_gather_cache import cached
|
||||||
from .extensions.gltf2_blender_image import Channel, ExportImage, FillImage
|
from .extensions.gltf2_blender_image import Channel, ExportImage, FillImage
|
||||||
from ..gltf2_blender_get import get_tex_from_socket
|
from .gltf2_blender_search_node_tree import get_texture_node_from_socket, NodeSocket
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def gather_image(
|
def gather_image(
|
||||||
@ -59,7 +59,7 @@ def gather_image(
|
|||||||
|
|
||||||
export_user_extensions('gather_image_hook', export_settings, image, blender_shader_sockets)
|
export_user_extensions('gather_image_hook', export_settings, image, blender_shader_sockets)
|
||||||
|
|
||||||
# We also return image_data, as it can be used to generate same file with another extension for webp management
|
# We also return image_data, as it can be used to generate same file with another extension for WebP management
|
||||||
return image, image_data, factor
|
return image, image_data, factor
|
||||||
|
|
||||||
def __gather_original_uri(original_uri, export_settings):
|
def __gather_original_uri(original_uri, export_settings):
|
||||||
@ -114,11 +114,11 @@ def __gather_extras(sockets, export_settings):
|
|||||||
def __gather_mime_type(sockets, export_image, export_settings):
|
def __gather_mime_type(sockets, export_image, export_settings):
|
||||||
# force png or webp if Alpha contained so we can export alpha
|
# force png or webp if Alpha contained so we can export alpha
|
||||||
for socket in sockets:
|
for socket in sockets:
|
||||||
if socket.name == "Alpha":
|
if socket.socket.name == "Alpha":
|
||||||
if export_settings["gltf_image_format"] == "WEBP":
|
if export_settings["gltf_image_format"] == "WEBP":
|
||||||
return "image/webp"
|
return "image/webp"
|
||||||
else:
|
else:
|
||||||
# If we keep image as is (no channel composition), we need to keep original format (for webp)
|
# If we keep image as is (no channel composition), we need to keep original format (for WebP)
|
||||||
image = export_image.blender_image()
|
image = export_image.blender_image()
|
||||||
if image is not None and __is_blender_image_a_webp(image):
|
if image is not None and __is_blender_image_a_webp(image):
|
||||||
return "image/webp"
|
return "image/webp"
|
||||||
@ -191,7 +191,7 @@ def __get_image_data(sockets, default_sockets, export_settings) -> ExportImage:
|
|||||||
# For shared resources, such as images, we just store the portion of data that is needed in the glTF property
|
# For shared resources, such as images, we just store the portion of data that is needed in the glTF property
|
||||||
# in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary
|
# in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary
|
||||||
# resources.
|
# resources.
|
||||||
results = [get_tex_from_socket(socket) for socket in sockets]
|
results = [get_texture_node_from_socket(socket, export_settings) for socket in sockets]
|
||||||
|
|
||||||
# Check if we need a simple mapping or more complex calculation
|
# Check if we need a simple mapping or more complex calculation
|
||||||
# There is currently no complex calculation for any textures
|
# There is currently no complex calculation for any textures
|
||||||
@ -222,7 +222,7 @@ def __get_image_data_mapping(sockets, default_sockets, results, export_settings)
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# rudimentarily try follow the node tree to find the correct image data.
|
# rudimentarily try follow the node tree to find the correct image data.
|
||||||
src_chan = Channel.R
|
src_chan = None
|
||||||
for elem in result.path:
|
for elem in result.path:
|
||||||
if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateColor):
|
if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateColor):
|
||||||
src_chan = {
|
src_chan = {
|
||||||
@ -233,26 +233,55 @@ def __get_image_data_mapping(sockets, default_sockets, results, export_settings)
|
|||||||
if elem.from_socket.name == 'Alpha':
|
if elem.from_socket.name == 'Alpha':
|
||||||
src_chan = Channel.A
|
src_chan = Channel.A
|
||||||
|
|
||||||
|
|
||||||
|
if src_chan is None:
|
||||||
|
# No SeparateColor node found, so take the specification channel that is needed
|
||||||
|
# So export is correct if user plug the texture directly to the socket
|
||||||
|
if socket.socket.name == 'Metallic':
|
||||||
|
src_chan = Channel.B
|
||||||
|
elif socket.socket.name == 'Roughness':
|
||||||
|
src_chan = Channel.G
|
||||||
|
elif socket.socket.name == 'Occlusion':
|
||||||
|
src_chan = Channel.R
|
||||||
|
elif socket.socket.name == 'Alpha':
|
||||||
|
src_chan = Channel.A
|
||||||
|
elif socket.socket.name == 'Coat Weight':
|
||||||
|
src_chan = Channel.R
|
||||||
|
elif socket.socket.name == 'Coat Roughness':
|
||||||
|
src_chan = Channel.G
|
||||||
|
elif socket.socket.name == 'Thickness': # For KHR_materials_volume
|
||||||
|
src_chan = Channel.G
|
||||||
|
|
||||||
|
if src_chan is None:
|
||||||
|
# Seems we can't find the channel
|
||||||
|
# We are in a case where user plugged a texture in a Color socket, but we may have used the alpha one
|
||||||
|
if socket.socket.name in ["Alpha", "Specular IOR Level", "Sheen Roughness"]:
|
||||||
|
src_chan = Channel.A
|
||||||
|
|
||||||
|
if src_chan is None:
|
||||||
|
# We definitely can't find the channel, so keep the first channel even if this is wrong
|
||||||
|
src_chan = Channel.R
|
||||||
|
|
||||||
dst_chan = None
|
dst_chan = None
|
||||||
|
|
||||||
# some sockets need channel rewriting (gltf pbr defines fixed channels for some attributes)
|
# some sockets need channel rewriting (gltf pbr defines fixed channels for some attributes)
|
||||||
if socket.name == 'Metallic':
|
if socket.socket.name == 'Metallic':
|
||||||
dst_chan = Channel.B
|
dst_chan = Channel.B
|
||||||
elif socket.name == 'Roughness':
|
elif socket.socket.name == 'Roughness':
|
||||||
dst_chan = Channel.G
|
dst_chan = Channel.G
|
||||||
elif socket.name == 'Occlusion':
|
elif socket.socket.name == 'Occlusion':
|
||||||
dst_chan = Channel.R
|
dst_chan = Channel.R
|
||||||
elif socket.name == 'Alpha':
|
elif socket.socket.name == 'Alpha':
|
||||||
dst_chan = Channel.A
|
dst_chan = Channel.A
|
||||||
elif socket.name == 'Coat Weight':
|
elif socket.socket.name == 'Coat Weight':
|
||||||
dst_chan = Channel.R
|
dst_chan = Channel.R
|
||||||
elif socket.name == 'Coat Roughness':
|
elif socket.socket.name == 'Coat Roughness':
|
||||||
dst_chan = Channel.G
|
dst_chan = Channel.G
|
||||||
elif socket.name == 'Thickness': # For KHR_materials_volume
|
elif socket.socket.name == 'Thickness': # For KHR_materials_volume
|
||||||
dst_chan = Channel.G
|
dst_chan = Channel.G
|
||||||
elif socket.name == "Specular IOR Level": # For KHR_material_specular
|
elif socket.socket.name == "Specular IOR Level": # For KHR_material_specular
|
||||||
dst_chan = Channel.A
|
dst_chan = Channel.A
|
||||||
elif socket.name == "Sheen Roughness": # For KHR_materials_sheen
|
elif socket.socket.name == "Sheen Roughness": # For KHR_materials_sheen
|
||||||
dst_chan = Channel.A
|
dst_chan = Channel.A
|
||||||
|
|
||||||
if dst_chan is not None:
|
if dst_chan is not None:
|
||||||
@ -260,12 +289,12 @@ def __get_image_data_mapping(sockets, default_sockets, results, export_settings)
|
|||||||
|
|
||||||
# Since metal/roughness are always used together, make sure
|
# Since metal/roughness are always used together, make sure
|
||||||
# the other channel is filled.
|
# the other channel is filled.
|
||||||
if socket.name == 'Metallic' and not composed_image.is_filled(Channel.G):
|
if socket.socket.name == 'Metallic' and not composed_image.is_filled(Channel.G):
|
||||||
if default_roughness is not None:
|
if default_roughness is not None:
|
||||||
composed_image.fill_with(Channel.G, default_roughness)
|
composed_image.fill_with(Channel.G, default_roughness)
|
||||||
else:
|
else:
|
||||||
composed_image.fill_white(Channel.G)
|
composed_image.fill_white(Channel.G)
|
||||||
elif socket.name == 'Roughness' and not composed_image.is_filled(Channel.B):
|
elif socket.socket.name == 'Roughness' and not composed_image.is_filled(Channel.B):
|
||||||
if default_metallic is not None:
|
if default_metallic is not None:
|
||||||
composed_image.fill_with(Channel.B, default_metallic)
|
composed_image.fill_with(Channel.B, default_metallic)
|
||||||
else:
|
else:
|
||||||
|
@ -10,7 +10,6 @@ from ....io.com.gltf2_io_extensions import Extension
|
|||||||
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
|
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
|
||||||
from ....io.com.gltf2_io_debug import print_console
|
from ....io.com.gltf2_io_debug import print_console
|
||||||
from ...com.gltf2_blender_extras import generate_extras
|
from ...com.gltf2_blender_extras import generate_extras
|
||||||
from ...exp import gltf2_blender_get
|
|
||||||
from ..gltf2_blender_gather_cache import cached, cached_by_key
|
from ..gltf2_blender_gather_cache import cached, cached_by_key
|
||||||
from . import gltf2_blender_gather_materials_unlit
|
from . import gltf2_blender_gather_materials_unlit
|
||||||
from . import gltf2_blender_gather_texture_info
|
from . import gltf2_blender_gather_texture_info
|
||||||
@ -23,6 +22,11 @@ from .extensions.gltf2_blender_gather_materials_specular import export_specular
|
|||||||
from .extensions.gltf2_blender_gather_materials_transmission import export_transmission
|
from .extensions.gltf2_blender_gather_materials_transmission import export_transmission
|
||||||
from .extensions.gltf2_blender_gather_materials_clearcoat import export_clearcoat
|
from .extensions.gltf2_blender_gather_materials_clearcoat import export_clearcoat
|
||||||
from .extensions.gltf2_blender_gather_materials_ior import export_ior
|
from .extensions.gltf2_blender_gather_materials_ior import export_ior
|
||||||
|
from .gltf2_blender_search_node_tree import \
|
||||||
|
has_image_node_from_socket, \
|
||||||
|
get_socket_from_gltf_material_node, \
|
||||||
|
get_socket, \
|
||||||
|
get_node_socket
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def get_material_cache_key(blender_material, export_settings):
|
def get_material_cache_key(blender_material, export_settings):
|
||||||
@ -90,7 +94,7 @@ def gather_material(blender_material, export_settings):
|
|||||||
# If emissive is set, from an emissive node (not PBR)
|
# If emissive is set, from an emissive node (not PBR)
|
||||||
# We need to set manually default values for
|
# We need to set manually default values for
|
||||||
# pbr_metallic_roughness.baseColor
|
# pbr_metallic_roughness.baseColor
|
||||||
if material.emissive_factor is not None and gltf2_blender_get.get_node_socket(blender_material, bpy.types.ShaderNodeBsdfPrincipled, "Base Color") is None:
|
if material.emissive_factor is not None and get_node_socket(blender_material, bpy.types.ShaderNodeBsdfPrincipled, "Base Color").socket is None:
|
||||||
material.pbr_metallic_roughness = gltf2_blender_gather_materials_pbr_metallic_roughness.get_default_pbr_for_emissive_node()
|
material.pbr_metallic_roughness = gltf2_blender_gather_materials_pbr_metallic_roughness.get_default_pbr_for_emissive_node()
|
||||||
|
|
||||||
export_user_extensions('gather_material_hook', export_settings, material, blender_material)
|
export_user_extensions('gather_material_hook', export_settings, material, blender_material)
|
||||||
@ -143,12 +147,6 @@ def __gather_double_sided(blender_material, extensions, export_settings):
|
|||||||
|
|
||||||
if not blender_material.use_backface_culling:
|
if not blender_material.use_backface_culling:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
old_double_sided_socket = gltf2_blender_get.get_socket_old(blender_material, "DoubleSided")
|
|
||||||
if old_double_sided_socket is not None and\
|
|
||||||
not old_double_sided_socket.is_linked and\
|
|
||||||
old_double_sided_socket.default_value > 0.5:
|
|
||||||
return True
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -222,9 +220,7 @@ def __gather_name(blender_material, export_settings):
|
|||||||
|
|
||||||
|
|
||||||
def __gather_normal_texture(blender_material, export_settings):
|
def __gather_normal_texture(blender_material, export_settings):
|
||||||
normal = gltf2_blender_get.get_socket(blender_material, "Normal")
|
normal = get_socket(blender_material, "Normal")
|
||||||
if normal is None:
|
|
||||||
normal = gltf2_blender_get.get_socket_old(blender_material, "Normal")
|
|
||||||
normal_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class(
|
normal_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class(
|
||||||
normal,
|
normal,
|
||||||
(normal,),
|
(normal,),
|
||||||
@ -236,35 +232,37 @@ def __gather_orm_texture(blender_material, export_settings):
|
|||||||
# Check for the presence of Occlusion, Roughness, Metallic sharing a single image.
|
# Check for the presence of Occlusion, Roughness, Metallic sharing a single image.
|
||||||
# If not fully shared, return None, so the images will be cached and processed separately.
|
# If not fully shared, return None, so the images will be cached and processed separately.
|
||||||
|
|
||||||
occlusion = gltf2_blender_get.get_socket(blender_material, "Occlusion")
|
occlusion = get_socket(blender_material, "Occlusion")
|
||||||
if occlusion is None or not gltf2_blender_get.has_image_node_from_socket(occlusion):
|
if occlusion.socket is None or not has_image_node_from_socket(occlusion, export_settings):
|
||||||
occlusion = gltf2_blender_get.get_socket_old(blender_material, "Occlusion")
|
occlusion = get_socket_from_gltf_material_node(blender_material, "Occlusion")
|
||||||
if occlusion is None or not gltf2_blender_get.has_image_node_from_socket(occlusion):
|
if occlusion.socket is None or not has_image_node_from_socket(occlusion, export_settings):
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
metallic_socket = gltf2_blender_get.get_socket(blender_material, "Metallic")
|
metallic_socket = get_socket(blender_material, "Metallic")
|
||||||
roughness_socket = gltf2_blender_get.get_socket(blender_material, "Roughness")
|
roughness_socket = get_socket(blender_material, "Roughness")
|
||||||
|
|
||||||
hasMetal = metallic_socket is not None and gltf2_blender_get.has_image_node_from_socket(metallic_socket)
|
hasMetal = metallic_socket.socket is not None and has_image_node_from_socket(metallic_socket, export_settings)
|
||||||
hasRough = roughness_socket is not None and gltf2_blender_get.has_image_node_from_socket(roughness_socket)
|
hasRough = roughness_socket.socket is not None and has_image_node_from_socket(roughness_socket, export_settings)
|
||||||
|
|
||||||
default_sockets = ()
|
default_sockets = ()
|
||||||
|
# Warning: for default socket, do not use NodeSocket object, because it will break cache
|
||||||
|
# Using directlty the Blender socket object
|
||||||
if not hasMetal and not hasRough:
|
if not hasMetal and not hasRough:
|
||||||
metallic_roughness = gltf2_blender_get.get_socket_old(blender_material, "MetallicRoughness")
|
metallic_roughness = get_socket_from_gltf_material_node(blender_material, "MetallicRoughness")
|
||||||
if metallic_roughness is None or not gltf2_blender_get.has_image_node_from_socket(metallic_roughness):
|
if metallic_roughness.socket is None or not has_image_node_from_socket(metallic_roughness, export_settings):
|
||||||
return None, default_sockets
|
return None, default_sockets
|
||||||
result = (occlusion, metallic_roughness)
|
result = (occlusion, metallic_roughness)
|
||||||
elif not hasMetal:
|
elif not hasMetal:
|
||||||
result = (occlusion, roughness_socket)
|
result = (occlusion, roughness_socket)
|
||||||
default_sockets = (metallic_socket,)
|
default_sockets = (metallic_socket.socket,)
|
||||||
elif not hasRough:
|
elif not hasRough:
|
||||||
result = (occlusion, metallic_socket)
|
result = (occlusion, metallic_socket)
|
||||||
default_sockets = (roughness_socket,)
|
default_sockets = (roughness_socket.socket,)
|
||||||
else:
|
else:
|
||||||
result = (occlusion, roughness_socket, metallic_socket)
|
result = (occlusion, roughness_socket, metallic_socket)
|
||||||
default_sockets = ()
|
default_sockets = ()
|
||||||
|
|
||||||
if not gltf2_blender_gather_texture_info.check_same_size_images(result):
|
if not gltf2_blender_gather_texture_info.check_same_size_images(result, export_settings):
|
||||||
print_console("INFO",
|
print_console("INFO",
|
||||||
"Occlusion and metal-roughness texture will be exported separately "
|
"Occlusion and metal-roughness texture will be exported separately "
|
||||||
"(use same-sized images if you want them combined)")
|
"(use same-sized images if you want them combined)")
|
||||||
@ -278,9 +276,9 @@ def __gather_orm_texture(blender_material, export_settings):
|
|||||||
return result, default_sockets
|
return result, default_sockets
|
||||||
|
|
||||||
def __gather_occlusion_texture(blender_material, orm_texture, default_sockets, export_settings):
|
def __gather_occlusion_texture(blender_material, orm_texture, default_sockets, export_settings):
|
||||||
occlusion = gltf2_blender_get.get_socket(blender_material, "Occlusion")
|
occlusion = get_socket(blender_material, "Occlusion")
|
||||||
if occlusion is None:
|
if occlusion.socket is None:
|
||||||
occlusion = gltf2_blender_get.get_socket_old(blender_material, "Occlusion")
|
occlusion = get_socket_from_gltf_material_node(blender_material, "Occlusion")
|
||||||
occlusion_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_material_occlusion_texture_info_class(
|
occlusion_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_material_occlusion_texture_info_class(
|
||||||
occlusion,
|
occlusion,
|
||||||
orm_texture or (occlusion,),
|
orm_texture or (occlusion,),
|
||||||
|
@ -4,13 +4,18 @@
|
|||||||
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from ....io.com import gltf2_io
|
from ....io.com import gltf2_io
|
||||||
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
|
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
|
||||||
from ...exp import gltf2_blender_get
|
|
||||||
from ..gltf2_blender_gather_cache import cached
|
from ..gltf2_blender_gather_cache import cached
|
||||||
from ..gltf2_blender_get import image_tex_is_valid_from_socket
|
|
||||||
from .gltf2_blender_search_node_tree import get_vertex_color_info
|
from .gltf2_blender_search_node_tree import get_vertex_color_info
|
||||||
from .gltf2_blender_gather_texture_info import gather_texture_info
|
from .gltf2_blender_gather_texture_info import gather_texture_info
|
||||||
|
from .gltf2_blender_search_node_tree import \
|
||||||
|
get_socket_from_gltf_material_node, \
|
||||||
|
has_image_node_from_socket, \
|
||||||
|
get_const_from_default_value_socket, \
|
||||||
|
get_socket, \
|
||||||
|
get_factor_from_socket
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def gather_material_pbr_metallic_roughness(blender_material, orm_texture, export_settings):
|
def gather_material_pbr_metallic_roughness(blender_material, orm_texture, export_settings):
|
||||||
@ -49,23 +54,23 @@ def __gather_base_color_factor(blender_material, export_settings):
|
|||||||
|
|
||||||
rgb, alpha = None, None
|
rgb, alpha = None, None
|
||||||
|
|
||||||
alpha_socket = gltf2_blender_get.get_socket(blender_material, "Alpha")
|
alpha_socket = get_socket(blender_material, "Alpha")
|
||||||
if isinstance(alpha_socket, bpy.types.NodeSocket):
|
if isinstance(alpha_socket.socket, bpy.types.NodeSocket):
|
||||||
if export_settings['gltf_image_format'] != "NONE":
|
if export_settings['gltf_image_format'] != "NONE":
|
||||||
alpha = gltf2_blender_get.get_factor_from_socket(alpha_socket, kind='VALUE')
|
alpha = get_factor_from_socket(alpha_socket, kind='VALUE')
|
||||||
else:
|
else:
|
||||||
alpha = gltf2_blender_get.get_const_from_default_value_socket(alpha_socket, kind='VALUE')
|
alpha = get_const_from_default_value_socket(alpha_socket, kind='VALUE')
|
||||||
|
|
||||||
base_color_socket = gltf2_blender_get.get_socket(blender_material, "Base Color")
|
base_color_socket = get_socket(blender_material, "Base Color")
|
||||||
|
if base_color_socket.socket is None:
|
||||||
|
base_color_socket = get_socket(blender_material, "BaseColor")
|
||||||
if base_color_socket is None:
|
if base_color_socket is None:
|
||||||
base_color_socket = gltf2_blender_get.get_socket(blender_material, "BaseColor")
|
base_color_socket = get_socket_from_gltf_material_node(blender_material, "BaseColorFactor")
|
||||||
if base_color_socket is None:
|
if isinstance(base_color_socket.socket, bpy.types.NodeSocket):
|
||||||
base_color_socket = gltf2_blender_get.get_socket_old(blender_material, "BaseColorFactor")
|
|
||||||
if isinstance(base_color_socket, bpy.types.NodeSocket):
|
|
||||||
if export_settings['gltf_image_format'] != "NONE":
|
if export_settings['gltf_image_format'] != "NONE":
|
||||||
rgb = gltf2_blender_get.get_factor_from_socket(base_color_socket, kind='RGB')
|
rgb = get_factor_from_socket(base_color_socket, kind='RGB')
|
||||||
else:
|
else:
|
||||||
rgb = gltf2_blender_get.get_const_from_default_value_socket(base_color_socket, kind='RGB')
|
rgb = get_const_from_default_value_socket(base_color_socket, kind='RGB')
|
||||||
|
|
||||||
if rgb is None: rgb = [1.0, 1.0, 1.0]
|
if rgb is None: rgb = [1.0, 1.0, 1.0]
|
||||||
if alpha is None: alpha = 1.0
|
if alpha is None: alpha = 1.0
|
||||||
@ -80,18 +85,18 @@ def __gather_base_color_factor(blender_material, export_settings):
|
|||||||
|
|
||||||
|
|
||||||
def __gather_base_color_texture(blender_material, export_settings):
|
def __gather_base_color_texture(blender_material, export_settings):
|
||||||
base_color_socket = gltf2_blender_get.get_socket(blender_material, "Base Color")
|
base_color_socket = get_socket(blender_material, "Base Color")
|
||||||
|
if base_color_socket.socket is None:
|
||||||
|
base_color_socket = get_socket(blender_material, "BaseColor")
|
||||||
if base_color_socket is None:
|
if base_color_socket is None:
|
||||||
base_color_socket = gltf2_blender_get.get_socket(blender_material, "BaseColor")
|
base_color_socket = get_socket_from_gltf_material_node(blender_material, "BaseColor")
|
||||||
if base_color_socket is None:
|
|
||||||
base_color_socket = gltf2_blender_get.get_socket_old(blender_material, "BaseColor")
|
|
||||||
|
|
||||||
alpha_socket = gltf2_blender_get.get_socket(blender_material, "Alpha")
|
alpha_socket = get_socket(blender_material, "Alpha")
|
||||||
|
|
||||||
# keep sockets that have some texture : color and/or alpha
|
# keep sockets that have some texture : color and/or alpha
|
||||||
inputs = tuple(
|
inputs = tuple(
|
||||||
socket for socket in [base_color_socket, alpha_socket]
|
socket for socket in [base_color_socket, alpha_socket]
|
||||||
if socket is not None and image_tex_is_valid_from_socket(socket)
|
if socket.socket is not None and has_image_node_from_socket(socket, export_settings)
|
||||||
)
|
)
|
||||||
if not inputs:
|
if not inputs:
|
||||||
return None, {}, {"uv_info": {}, "vc_info": {}}, None
|
return None, {}, {"uv_info": {}, "vc_info": {}}, None
|
||||||
@ -113,34 +118,35 @@ def __gather_metallic_factor(blender_material, export_settings):
|
|||||||
if not blender_material.use_nodes:
|
if not blender_material.use_nodes:
|
||||||
return blender_material.metallic
|
return blender_material.metallic
|
||||||
|
|
||||||
metallic_socket = gltf2_blender_get.get_socket(blender_material, "Metallic")
|
metallic_socket = get_socket(blender_material, "Metallic")
|
||||||
if metallic_socket is None:
|
if metallic_socket is None:
|
||||||
metallic_socket = gltf2_blender_get.get_socket_old(blender_material, "MetallicFactor")
|
metallic_socket = get_socket_from_gltf_material_node(blender_material, "MetallicFactor")
|
||||||
if isinstance(metallic_socket, bpy.types.NodeSocket):
|
if isinstance(metallic_socket.socket, bpy.types.NodeSocket):
|
||||||
fac = gltf2_blender_get.get_factor_from_socket(metallic_socket, kind='VALUE')
|
fac = get_factor_from_socket(metallic_socket, kind='VALUE')
|
||||||
return fac if fac != 1 else None
|
return fac if fac != 1 else None
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def __gather_metallic_roughness_texture(blender_material, orm_texture, export_settings):
|
def __gather_metallic_roughness_texture(blender_material, orm_texture, export_settings):
|
||||||
metallic_socket = gltf2_blender_get.get_socket(blender_material, "Metallic")
|
metallic_socket = get_socket(blender_material, "Metallic")
|
||||||
roughness_socket = gltf2_blender_get.get_socket(blender_material, "Roughness")
|
roughness_socket = get_socket(blender_material, "Roughness")
|
||||||
|
|
||||||
hasMetal = metallic_socket is not None and image_tex_is_valid_from_socket(metallic_socket)
|
hasMetal = metallic_socket.socket is not None and has_image_node_from_socket(metallic_socket, export_settings)
|
||||||
hasRough = roughness_socket is not None and image_tex_is_valid_from_socket(roughness_socket)
|
hasRough = roughness_socket.socket is not None and has_image_node_from_socket(roughness_socket, export_settings)
|
||||||
|
|
||||||
default_sockets = ()
|
default_sockets = ()
|
||||||
|
# Warning: for default socket, do not use NodeSocket object, because it will break cache
|
||||||
|
# Using directlty the Blender socket object
|
||||||
if not hasMetal and not hasRough:
|
if not hasMetal and not hasRough:
|
||||||
metallic_roughness = gltf2_blender_get.get_socket_old(blender_material, "MetallicRoughness")
|
metallic_roughness = get_socket_from_gltf_material_node(blender_material, "MetallicRoughness")
|
||||||
if metallic_roughness is None or not image_tex_is_valid_from_socket(metallic_roughness):
|
if metallic_roughness is None or not has_image_node_from_socket(metallic_roughness, export_settings):
|
||||||
return None, {}, None
|
return None, {}, None
|
||||||
texture_input = (metallic_roughness,)
|
|
||||||
elif not hasMetal:
|
elif not hasMetal:
|
||||||
texture_input = (roughness_socket,)
|
texture_input = (roughness_socket,)
|
||||||
default_sockets = (metallic_socket,)
|
default_sockets = (metallic_socket.socket,)
|
||||||
elif not hasRough:
|
elif not hasRough:
|
||||||
texture_input = (metallic_socket,)
|
texture_input = (metallic_socket,)
|
||||||
default_sockets = (roughness_socket,)
|
default_sockets = (roughness_socket.socket,)
|
||||||
else:
|
else:
|
||||||
texture_input = (metallic_socket, roughness_socket)
|
texture_input = (metallic_socket, roughness_socket)
|
||||||
default_sockets = ()
|
default_sockets = ()
|
||||||
@ -158,11 +164,11 @@ def __gather_roughness_factor(blender_material, export_settings):
|
|||||||
if not blender_material.use_nodes:
|
if not blender_material.use_nodes:
|
||||||
return blender_material.roughness
|
return blender_material.roughness
|
||||||
|
|
||||||
roughness_socket = gltf2_blender_get.get_socket(blender_material, "Roughness")
|
roughness_socket = get_socket(blender_material, "Roughness")
|
||||||
if roughness_socket is None:
|
if roughness_socket is None:
|
||||||
roughness_socket = gltf2_blender_get.get_socket_old(blender_material, "RoughnessFactor")
|
roughness_socket = get_socket_from_gltf_material_node(blender_material, "RoughnessFactor")
|
||||||
if isinstance(roughness_socket, bpy.types.NodeSocket):
|
if isinstance(roughness_socket.socket, bpy.types.NodeSocket):
|
||||||
fac = gltf2_blender_get.get_factor_from_socket(roughness_socket, kind='VALUE')
|
fac = get_factor_from_socket(roughness_socket, kind='VALUE')
|
||||||
return fac if fac != 1 else None
|
return fac if fac != 1 else None
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -2,10 +2,14 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from ....io.com.gltf2_io_extensions import Extension
|
|
||||||
from ...exp import gltf2_blender_get
|
|
||||||
from . import gltf2_blender_gather_texture_info
|
from . import gltf2_blender_gather_texture_info
|
||||||
from .gltf2_blender_search_node_tree import get_vertex_color_info
|
from .gltf2_blender_search_node_tree import get_vertex_color_info
|
||||||
|
from .gltf2_blender_search_node_tree import \
|
||||||
|
get_socket, \
|
||||||
|
NodeSocket, \
|
||||||
|
previous_socket, \
|
||||||
|
previous_node, \
|
||||||
|
get_factor_from_socket
|
||||||
|
|
||||||
def detect_shadeless_material(blender_material, export_settings):
|
def detect_shadeless_material(blender_material, export_settings):
|
||||||
"""Detect if this material is "shadeless" ie. should be exported
|
"""Detect if this material is "shadeless" ie. should be exported
|
||||||
@ -15,8 +19,8 @@ def detect_shadeless_material(blender_material, export_settings):
|
|||||||
if not blender_material.use_nodes: return None
|
if not blender_material.use_nodes: return None
|
||||||
|
|
||||||
# Old Background node detection (unlikely to happen)
|
# Old Background node detection (unlikely to happen)
|
||||||
bg_socket = gltf2_blender_get.get_socket(blender_material, "Background")
|
bg_socket = get_socket(blender_material, "Background")
|
||||||
if bg_socket is not None:
|
if bg_socket.socket is not None:
|
||||||
return {'rgb_socket': bg_socket}
|
return {'rgb_socket': bg_socket}
|
||||||
|
|
||||||
# Look for
|
# Look for
|
||||||
@ -27,6 +31,7 @@ def detect_shadeless_material(blender_material, export_settings):
|
|||||||
|
|
||||||
info = {}
|
info = {}
|
||||||
|
|
||||||
|
#TODOSNode this can be a function call
|
||||||
for node in blender_material.node_tree.nodes:
|
for node in blender_material.node_tree.nodes:
|
||||||
if node.type == 'OUTPUT_MATERIAL' and node.is_active_output:
|
if node.type == 'OUTPUT_MATERIAL' and node.is_active_output:
|
||||||
socket = node.inputs[0]
|
socket = node.inputs[0]
|
||||||
@ -34,6 +39,8 @@ def detect_shadeless_material(blender_material, export_settings):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
socket = NodeSocket(socket, [blender_material])
|
||||||
|
|
||||||
# Be careful not to misidentify a lightpath trick as mix-alpha.
|
# Be careful not to misidentify a lightpath trick as mix-alpha.
|
||||||
result = __detect_lightpath_trick(socket)
|
result = __detect_lightpath_trick(socket)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
@ -49,10 +56,10 @@ def detect_shadeless_material(blender_material, export_settings):
|
|||||||
socket = result['next_socket']
|
socket = result['next_socket']
|
||||||
|
|
||||||
# Check if a color socket, or connected to a color socket
|
# Check if a color socket, or connected to a color socket
|
||||||
if socket.type != 'RGBA':
|
if socket.socket.type != 'RGBA':
|
||||||
from_socket = gltf2_blender_get.previous_socket(socket)
|
from_socket = previous_socket(socket)
|
||||||
if from_socket is None: return None
|
if from_socket.socket is None: return None
|
||||||
if from_socket.type != 'RGBA': return None
|
if from_socket.socket.type != 'RGBA': return None
|
||||||
|
|
||||||
info['rgb_socket'] = socket
|
info['rgb_socket'] = socket
|
||||||
return info
|
return info
|
||||||
@ -68,13 +75,13 @@ def __detect_mix_alpha(socket):
|
|||||||
#
|
#
|
||||||
# Returns None if not detected. Otherwise, a dict containing alpha_socket
|
# Returns None if not detected. Otherwise, a dict containing alpha_socket
|
||||||
# and next_socket.
|
# and next_socket.
|
||||||
prev = gltf2_blender_get.previous_node(socket)
|
prev = previous_node(socket)
|
||||||
if prev is None or prev.type != 'MIX_SHADER': return None
|
if prev.node is None or prev.node.type != 'MIX_SHADER': return None
|
||||||
in1 = gltf2_blender_get.previous_node(prev.inputs[1])
|
in1 = previous_node(NodeSocket(prev.node.inputs[1], prev.group_path))
|
||||||
if in1 is None or in1.type != 'BSDF_TRANSPARENT': return None
|
if in1.node is None or in1.node.type != 'BSDF_TRANSPARENT': return None
|
||||||
return {
|
return {
|
||||||
'alpha_socket': prev.inputs[0],
|
'alpha_socket': NodeSocket(prev.node.inputs[0], prev.group_path),
|
||||||
'next_socket': prev.inputs[2],
|
'next_socket': NodeSocket(prev.node.inputs[2], prev.group_path),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -90,17 +97,17 @@ def __detect_lightpath_trick(socket):
|
|||||||
# The Emission node can be omitted.
|
# The Emission node can be omitted.
|
||||||
# Returns None if not detected. Otherwise, a dict containing
|
# Returns None if not detected. Otherwise, a dict containing
|
||||||
# next_socket.
|
# next_socket.
|
||||||
prev = gltf2_blender_get.previous_node(socket)
|
prev = previous_node(socket)
|
||||||
if prev is None or prev.type != 'MIX_SHADER': return None
|
if prev.node is None or prev.node.type != 'MIX_SHADER': return None
|
||||||
in0 = gltf2_blender_get.previous_socket(prev.inputs[0])
|
in0 = previous_socket(NodeSocket(prev.node.inputs[0], prev.group_path))
|
||||||
if in0 is None or in0.node.type != 'LIGHT_PATH': return None
|
if in0.socket is None or in0.socket.node.type != 'LIGHT_PATH': return None
|
||||||
if in0.name != 'Is Camera Ray': return None
|
if in0.socket.name != 'Is Camera Ray': return None
|
||||||
next_socket = prev.inputs[2]
|
next_socket = NodeSocket(prev.node.inputs[2], prev.group_path)
|
||||||
|
|
||||||
# Detect emission
|
# Detect emission
|
||||||
prev = gltf2_blender_get.previous_node(next_socket)
|
prev = previous_node(next_socket)
|
||||||
if prev is not None and prev.type == 'EMISSION':
|
if prev.node is not None and prev.node.type == 'EMISSION':
|
||||||
next_socket = prev.inputs[0]
|
next_socket = NodeSocket(prev.node.inputs[0], prev.group_path)
|
||||||
|
|
||||||
return {'next_socket': next_socket}
|
return {'next_socket': next_socket}
|
||||||
|
|
||||||
@ -109,9 +116,9 @@ def gather_base_color_factor(info, export_settings):
|
|||||||
rgb, alpha = None, None
|
rgb, alpha = None, None
|
||||||
|
|
||||||
if 'rgb_socket' in info:
|
if 'rgb_socket' in info:
|
||||||
rgb = gltf2_blender_get.get_factor_from_socket(info['rgb_socket'], kind='RGB')
|
rgb = get_factor_from_socket(info['rgb_socket'], kind='RGB')
|
||||||
if 'alpha_socket' in info:
|
if 'alpha_socket' in info:
|
||||||
alpha = gltf2_blender_get.get_factor_from_socket(info['alpha_socket'], kind='VALUE')
|
alpha = get_factor_from_socket(info['alpha_socket'], kind='VALUE')
|
||||||
|
|
||||||
if rgb is None: rgb = [1.0, 1.0, 1.0]
|
if rgb is None: rgb = [1.0, 1.0, 1.0]
|
||||||
if alpha is None: alpha = 1.0
|
if alpha is None: alpha = 1.0
|
||||||
@ -122,8 +129,8 @@ def gather_base_color_factor(info, export_settings):
|
|||||||
|
|
||||||
|
|
||||||
def gather_base_color_texture(info, export_settings):
|
def gather_base_color_texture(info, export_settings):
|
||||||
sockets = (info.get('rgb_socket'), info.get('alpha_socket'))
|
sockets = (info.get('rgb_socket', NodeSocket(None, None)), info.get('alpha_socket', NodeSocket(None, None)))
|
||||||
sockets = tuple(s for s in sockets if s is not None)
|
sockets = tuple(s for s in sockets if s.socket is not None)
|
||||||
if sockets:
|
if sockets:
|
||||||
# NOTE: separate RGB and Alpha textures will not get combined
|
# NOTE: separate RGB and Alpha textures will not get combined
|
||||||
# because gather_image determines how to pack images based on the
|
# because gather_image determines how to pack images based on the
|
||||||
|
@ -4,21 +4,23 @@
|
|||||||
|
|
||||||
import typing
|
import typing
|
||||||
import bpy
|
import bpy
|
||||||
from ....io.com import gltf2_io_debug
|
|
||||||
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
|
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
|
||||||
from ....io.com.gltf2_io_extensions import Extension
|
from ....io.com.gltf2_io_extensions import Extension
|
||||||
from ....io.exp.gltf2_io_image_data import ImageData
|
from ....io.exp.gltf2_io_image_data import ImageData
|
||||||
from ....io.exp.gltf2_io_binary_data import BinaryData
|
from ....io.exp.gltf2_io_binary_data import BinaryData
|
||||||
|
from ....io.com import gltf2_io_debug
|
||||||
from ....io.com import gltf2_io
|
from ....io.com import gltf2_io
|
||||||
from ..gltf2_blender_gather_cache import cached
|
|
||||||
from ..gltf2_blender_gather_sampler import gather_sampler
|
from ..gltf2_blender_gather_sampler import gather_sampler
|
||||||
from ..gltf2_blender_get import get_tex_from_socket
|
from ..gltf2_blender_gather_cache import cached
|
||||||
|
from .gltf2_blender_search_node_tree import get_texture_node_from_socket, NodeSocket
|
||||||
from . import gltf2_blender_gather_image
|
from . import gltf2_blender_gather_image
|
||||||
|
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def gather_texture(
|
def gather_texture(
|
||||||
blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket],
|
blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket],
|
||||||
default_sockets: typing.Tuple[bpy.types.NodeSocket],
|
default_sockets,
|
||||||
export_settings):
|
export_settings):
|
||||||
"""
|
"""
|
||||||
Gather texture sampling information and image channels from a blender shader texture attached to a shader socket.
|
Gather texture sampling information and image channels from a blender shader texture attached to a shader socket.
|
||||||
@ -70,7 +72,7 @@ def __gather_extensions(blender_shader_sockets, source, webp_image, image_data,
|
|||||||
|
|
||||||
ext_webp = {}
|
ext_webp = {}
|
||||||
|
|
||||||
# If user want to keep original textures, and these textures are webp, we need to remove source from
|
# If user want to keep original textures, and these textures are WebP, we need to remove source from
|
||||||
# gltf2_io.Texture, and populate extension
|
# gltf2_io.Texture, and populate extension
|
||||||
if export_settings['gltf_keep_original_textures'] is True \
|
if export_settings['gltf_keep_original_textures'] is True \
|
||||||
and source is not None \
|
and source is not None \
|
||||||
@ -79,19 +81,19 @@ def __gather_extensions(blender_shader_sockets, source, webp_image, image_data,
|
|||||||
remove_source = True
|
remove_source = True
|
||||||
required = True
|
required = True
|
||||||
|
|
||||||
# If user want to export in webp format (so without fallback in png/jpg)
|
# If user want to export in WebP format (so without fallback in png/jpg)
|
||||||
if export_settings['gltf_image_format'] == "WEBP":
|
if export_settings['gltf_image_format'] == "WEBP":
|
||||||
# We create all image without fallback
|
# We create all image without fallback
|
||||||
ext_webp["source"] = source
|
ext_webp["source"] = source
|
||||||
remove_source = True
|
remove_source = True
|
||||||
required = True
|
required = True
|
||||||
|
|
||||||
# If user doesn't want to export in webp format, but want webp too. Texture is not webp
|
# If user doesn't want to export in WebP format, but want WebP too. Texture is not WebP
|
||||||
if export_settings['gltf_image_format'] != "WEBP" \
|
if export_settings['gltf_image_format'] != "WEBP" \
|
||||||
and export_settings['gltf_add_webp'] \
|
and export_settings['gltf_add_webp'] \
|
||||||
and source is not None \
|
and source is not None \
|
||||||
and source.mime_type != "image/webp":
|
and source.mime_type != "image/webp":
|
||||||
# We need here to create some webp textures
|
# We need here to create some WebP textures
|
||||||
|
|
||||||
new_mime_type = "image/webp"
|
new_mime_type = "image/webp"
|
||||||
new_data, _ = image_data.encode(new_mime_type, export_settings)
|
new_data, _ = image_data.encode(new_mime_type, export_settings)
|
||||||
@ -116,7 +118,7 @@ def __gather_extensions(blender_shader_sockets, source, webp_image, image_data,
|
|||||||
ext_webp["source"] = webp_image
|
ext_webp["source"] = webp_image
|
||||||
|
|
||||||
|
|
||||||
# If user doesn't want to export in webp format, but want webp too. Texture is webp
|
# If user doesn't want to export in WebP format, but want WebP too. Texture is WebP
|
||||||
if export_settings['gltf_image_format'] != "WEBP" \
|
if export_settings['gltf_image_format'] != "WEBP" \
|
||||||
and source is not None \
|
and source is not None \
|
||||||
and source.mime_type == "image/webp":
|
and source.mime_type == "image/webp":
|
||||||
@ -127,7 +129,7 @@ def __gather_extensions(blender_shader_sockets, source, webp_image, image_data,
|
|||||||
remove_source = True
|
remove_source = True
|
||||||
required = True
|
required = True
|
||||||
|
|
||||||
# If user doesn't want to export in webp format, but want webp too as fallback. Texture is webp
|
# If user doesn't want to export in webp format, but want WebP too as fallback. Texture is WebP
|
||||||
if export_settings['gltf_image_format'] != "WEBP" \
|
if export_settings['gltf_image_format'] != "WEBP" \
|
||||||
and webp_image is not None \
|
and webp_image is not None \
|
||||||
and export_settings['gltf_webp_fallback'] is True:
|
and export_settings['gltf_webp_fallback'] is True:
|
||||||
@ -164,14 +166,33 @@ def __gather_name(blender_shader_sockets, export_settings):
|
|||||||
|
|
||||||
|
|
||||||
def __gather_sampler(blender_shader_sockets, export_settings):
|
def __gather_sampler(blender_shader_sockets, export_settings):
|
||||||
shader_nodes = [get_tex_from_socket(socket) for socket in blender_shader_sockets]
|
shader_nodes = [get_texture_node_from_socket(socket, export_settings) for socket in blender_shader_sockets]
|
||||||
if len(shader_nodes) > 1:
|
if len(shader_nodes) > 1:
|
||||||
gltf2_io_debug.print_console("WARNING",
|
gltf2_io_debug.print_console("WARNING",
|
||||||
"More than one shader node tex image used for a texture. "
|
"More than one shader node tex image used for a texture. "
|
||||||
"The resulting glTF sampler will behave like the first shader node tex image.")
|
"The resulting glTF sampler will behave like the first shader node tex image.")
|
||||||
first_valid_shader_node = next(filter(lambda x: x is not None, shader_nodes)).shader_node
|
first_valid_shader_node = next(filter(lambda x: x is not None, shader_nodes))
|
||||||
|
|
||||||
|
# group_path can't be a list, so transform it to str
|
||||||
|
|
||||||
|
sep_item = "##~~gltf-sep~~##"
|
||||||
|
sep_inside_item = "##~~gltf-inside-sep~~##"
|
||||||
|
group_path_str = ""
|
||||||
|
if len(first_valid_shader_node.group_path) > 0:
|
||||||
|
group_path_str += first_valid_shader_node.group_path[0].name
|
||||||
|
if len(first_valid_shader_node.group_path) > 1:
|
||||||
|
for idx, i in enumerate(first_valid_shader_node.group_path[1:]):
|
||||||
|
group_path_str += sep_item
|
||||||
|
if idx == 0:
|
||||||
|
group_path_str += first_valid_shader_node.group_path[0].name
|
||||||
|
else:
|
||||||
|
group_path_str += i.id_data.name
|
||||||
|
group_path_str += sep_inside_item
|
||||||
|
group_path_str += i.name
|
||||||
|
|
||||||
return gather_sampler(
|
return gather_sampler(
|
||||||
first_valid_shader_node,
|
first_valid_shader_node.shader_node,
|
||||||
|
group_path_str,
|
||||||
export_settings)
|
export_settings)
|
||||||
|
|
||||||
|
|
||||||
@ -209,7 +230,7 @@ def __gather_source(blender_shader_sockets, default_sockets, export_settings):
|
|||||||
|
|
||||||
png_image = __make_webp_image(buffer_view, None, None, new_mime_type, name, uri, export_settings)
|
png_image = __make_webp_image(buffer_view, None, None, new_mime_type, name, uri, export_settings)
|
||||||
|
|
||||||
# We inverted the png & webp image, to have the png as main source
|
# We inverted the png & WebP image, to have the png as main source
|
||||||
return png_image, source, image_data, factor
|
return png_image, source, image_data, factor
|
||||||
return source, None, image_data, factor
|
return source, None, image_data, factor
|
||||||
|
|
||||||
|
@ -7,12 +7,17 @@ import typing
|
|||||||
from ....io.com import gltf2_io
|
from ....io.com import gltf2_io
|
||||||
from ....io.com.gltf2_io_extensions import Extension
|
from ....io.com.gltf2_io_extensions import Extension
|
||||||
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
|
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
|
||||||
from ...exp import gltf2_blender_get
|
|
||||||
from ..gltf2_blender_get import previous_node, get_tex_from_socket
|
|
||||||
from ..gltf2_blender_gather_sampler import detect_manual_uv_wrapping
|
from ..gltf2_blender_gather_sampler import detect_manual_uv_wrapping
|
||||||
from ..gltf2_blender_gather_cache import cached
|
from ..gltf2_blender_gather_cache import cached
|
||||||
from . import gltf2_blender_gather_texture
|
from . import gltf2_blender_gather_texture
|
||||||
from . import gltf2_blender_search_node_tree
|
from .gltf2_blender_search_node_tree import \
|
||||||
|
get_texture_node_from_socket, \
|
||||||
|
from_socket, \
|
||||||
|
FilterByType, \
|
||||||
|
previous_node, \
|
||||||
|
get_const_from_socket, \
|
||||||
|
NodeSocket, \
|
||||||
|
get_texture_transform_from_mapping_node
|
||||||
|
|
||||||
# blender_shader_sockets determine the texture and primary_socket determines
|
# blender_shader_sockets determine the texture and primary_socket determines
|
||||||
# the textransform and UVMap. Ex: when combining an ORM texture, for
|
# the textransform and UVMap. Ex: when combining an ORM texture, for
|
||||||
@ -37,7 +42,7 @@ def gather_material_occlusion_texture_info_class(primary_socket, blender_shader_
|
|||||||
def __gather_texture_info_helper(
|
def __gather_texture_info_helper(
|
||||||
primary_socket: bpy.types.NodeSocket,
|
primary_socket: bpy.types.NodeSocket,
|
||||||
blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket],
|
blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket],
|
||||||
default_sockets: typing.Tuple[bpy.types.NodeSocket],
|
default_sockets,
|
||||||
kind: str,
|
kind: str,
|
||||||
filter_type: str,
|
filter_type: str,
|
||||||
export_settings):
|
export_settings):
|
||||||
@ -77,7 +82,7 @@ def __gather_texture_info_helper(
|
|||||||
def __filter_texture_info(primary_socket, blender_shader_sockets, filter_type, export_settings):
|
def __filter_texture_info(primary_socket, blender_shader_sockets, filter_type, export_settings):
|
||||||
if primary_socket is None:
|
if primary_socket is None:
|
||||||
return False
|
return False
|
||||||
if get_tex_from_socket(primary_socket) is None:
|
if get_texture_node_from_socket(primary_socket, export_settings) is None:
|
||||||
return False
|
return False
|
||||||
if not blender_shader_sockets:
|
if not blender_shader_sockets:
|
||||||
return False
|
return False
|
||||||
@ -85,12 +90,12 @@ def __filter_texture_info(primary_socket, blender_shader_sockets, filter_type, e
|
|||||||
return False
|
return False
|
||||||
if filter_type == "ALL":
|
if filter_type == "ALL":
|
||||||
# Check that all sockets link to texture
|
# Check that all sockets link to texture
|
||||||
if any([get_tex_from_socket(socket) is None for socket in blender_shader_sockets]):
|
if any([get_texture_node_from_socket(socket, export_settings) is None for socket in blender_shader_sockets]):
|
||||||
# sockets do not lead to a texture --> discard
|
# sockets do not lead to a texture --> discard
|
||||||
return False
|
return False
|
||||||
elif filter_type == "ANY":
|
elif filter_type == "ANY":
|
||||||
# Check that at least one socket link to texture
|
# Check that at least one socket link to texture
|
||||||
if all([get_tex_from_socket(socket) is None for socket in blender_shader_sockets]):
|
if all([get_texture_node_from_socket(socket, export_settings) is None for socket in blender_shader_sockets]):
|
||||||
return False
|
return False
|
||||||
elif filter_type == "NONE":
|
elif filter_type == "NONE":
|
||||||
# No check
|
# No check
|
||||||
@ -112,9 +117,9 @@ def __gather_extras(blender_shader_sockets, export_settings):
|
|||||||
|
|
||||||
# MaterialNormalTextureInfo only
|
# MaterialNormalTextureInfo only
|
||||||
def __gather_normal_scale(primary_socket, export_settings):
|
def __gather_normal_scale(primary_socket, export_settings):
|
||||||
result = gltf2_blender_search_node_tree.from_socket(
|
result = from_socket(
|
||||||
primary_socket,
|
primary_socket,
|
||||||
gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeNormalMap))
|
FilterByType(bpy.types.ShaderNodeNormalMap))
|
||||||
if not result:
|
if not result:
|
||||||
return None
|
return None
|
||||||
strengthInput = result[0].shader_node.inputs['Strength']
|
strengthInput = result[0].shader_node.inputs['Strength']
|
||||||
@ -127,11 +132,11 @@ def __gather_normal_scale(primary_socket, export_settings):
|
|||||||
def __gather_occlusion_strength(primary_socket, export_settings):
|
def __gather_occlusion_strength(primary_socket, export_settings):
|
||||||
# Look for a MixRGB node that mixes with pure white in front of
|
# Look for a MixRGB node that mixes with pure white in front of
|
||||||
# primary_socket. The mix factor gives the occlusion strength.
|
# primary_socket. The mix factor gives the occlusion strength.
|
||||||
node = gltf2_blender_get.previous_node(primary_socket)
|
node = previous_node(primary_socket)
|
||||||
if node and node.type == 'MIX' and node.blend_type == 'MIX':
|
if node and node.node.type == 'MIX' and node.node.blend_type == 'MIX':
|
||||||
fac = gltf2_blender_get.get_const_from_socket(node.inputs['Factor'], kind='VALUE')
|
fac = get_const_from_socket(NodeSocket(node.node.inputs['Factor'], node.group_path), kind='VALUE')
|
||||||
col1 = gltf2_blender_get.get_const_from_socket(node.inputs[6], kind='RGB')
|
col1 = get_const_from_socket(NodeSocket(node.node.inputs[6], node.group_path), kind='RGB')
|
||||||
col2 = gltf2_blender_get.get_const_from_socket(node.inputs[7], kind='RGB')
|
col2 = get_const_from_socket(NodeSocket(node.node.inputs[7], node.group_path), kind='RGB')
|
||||||
if fac is not None:
|
if fac is not None:
|
||||||
if col1 == [1.0, 1.0, 1.0] and col2 is None:
|
if col1 == [1.0, 1.0, 1.0] and col2 is None:
|
||||||
return fac
|
return fac
|
||||||
@ -153,31 +158,32 @@ def __gather_texture_transform_and_tex_coord(primary_socket, export_settings):
|
|||||||
#
|
#
|
||||||
# The [UV Wrapping] is for wrap modes like MIRROR that use nodes,
|
# The [UV Wrapping] is for wrap modes like MIRROR that use nodes,
|
||||||
# [Mapping] is for KHR_texture_transform, and [UV Map] is for texCoord.
|
# [Mapping] is for KHR_texture_transform, and [UV Map] is for texCoord.
|
||||||
blender_shader_node = get_tex_from_socket(primary_socket).shader_node
|
result_tex = get_texture_node_from_socket(primary_socket, export_settings)
|
||||||
|
blender_shader_node = result_tex.shader_node
|
||||||
|
|
||||||
# Skip over UV wrapping stuff (it goes in the sampler)
|
# Skip over UV wrapping stuff (it goes in the sampler)
|
||||||
result = detect_manual_uv_wrapping(blender_shader_node)
|
result = detect_manual_uv_wrapping(blender_shader_node, result_tex.group_path)
|
||||||
if result:
|
if result:
|
||||||
node = previous_node(result['next_socket'])
|
node = previous_node(result['next_socket'])
|
||||||
else:
|
else:
|
||||||
node = previous_node(blender_shader_node.inputs['Vector'])
|
node = previous_node(NodeSocket(blender_shader_node.inputs['Vector'], result_tex.group_path))
|
||||||
|
|
||||||
texture_transform = None
|
texture_transform = None
|
||||||
if node and node.type == 'MAPPING':
|
if node.node and node.node.type == 'MAPPING':
|
||||||
texture_transform = gltf2_blender_get.get_texture_transform_from_mapping_node(node)
|
texture_transform = get_texture_transform_from_mapping_node(node)
|
||||||
node = previous_node(node.inputs['Vector'])
|
node = previous_node(NodeSocket(node.node.inputs['Vector'], node.group_path))
|
||||||
|
|
||||||
uvmap_info = {}
|
uvmap_info = {}
|
||||||
|
|
||||||
if node and node.type == 'UVMAP' and node.uv_map:
|
if node.node and node.node.type == 'UVMAP' and node.node.uv_map:
|
||||||
uvmap_info['type'] = "Fixed"
|
uvmap_info['type'] = "Fixed"
|
||||||
uvmap_info['value'] = node.uv_map
|
uvmap_info['value'] = node.node.uv_map
|
||||||
|
|
||||||
elif node and node.type == 'ATTRIBUTE' \
|
elif node and node.node and node.node.type == 'ATTRIBUTE' \
|
||||||
and node.attribute_type == "GEOMETRY" \
|
and node.node.attribute_type == "GEOMETRY" \
|
||||||
and node.attribute_name:
|
and node.node.attribute_name:
|
||||||
uvmap_info['type'] = 'Attribute'
|
uvmap_info['type'] = 'Attribute'
|
||||||
uvmap_info['value'] = node.attribute_name
|
uvmap_info['value'] = node.node.attribute_name
|
||||||
|
|
||||||
else:
|
else:
|
||||||
uvmap_info['type'] = 'Active'
|
uvmap_info['type'] = 'Active'
|
||||||
@ -187,6 +193,7 @@ def __gather_texture_transform_and_tex_coord(primary_socket, export_settings):
|
|||||||
|
|
||||||
def check_same_size_images(
|
def check_same_size_images(
|
||||||
blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket],
|
blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket],
|
||||||
|
export_settings
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check that all sockets leads to images of the same size."""
|
"""Check that all sockets leads to images of the same size."""
|
||||||
if not blender_shader_sockets or not all(blender_shader_sockets):
|
if not blender_shader_sockets or not all(blender_shader_sockets):
|
||||||
@ -194,7 +201,7 @@ def check_same_size_images(
|
|||||||
|
|
||||||
sizes = set()
|
sizes = set()
|
||||||
for socket in blender_shader_sockets:
|
for socket in blender_shader_sockets:
|
||||||
tex = get_tex_from_socket(socket)
|
tex = get_texture_node_from_socket(socket, export_settings)
|
||||||
if tex is None:
|
if tex is None:
|
||||||
return False
|
return False
|
||||||
size = tex.shader_node.image.size
|
size = tex.shader_node.image.size
|
||||||
|
@ -7,6 +7,11 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
from mathutils import Vector, Matrix
|
||||||
|
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
|
||||||
|
from ...com.gltf2_blender_material_helpers import get_gltf_node_name, get_gltf_node_old_name, get_gltf_old_group_node_name
|
||||||
|
from ....blender.com.gltf2_blender_conversion import texture_transform_blender_to_gltf
|
||||||
|
from io_scene_gltf2.io.com import gltf2_io_debug
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
|
||||||
@ -48,13 +53,14 @@ class FilterByType(Filter):
|
|||||||
|
|
||||||
|
|
||||||
class NodeTreeSearchResult:
|
class NodeTreeSearchResult:
|
||||||
def __init__(self, shader_node: bpy.types.Node, path: typing.List[bpy.types.NodeLink]):
|
def __init__(self, shader_node: bpy.types.Node, path: typing.List[bpy.types.NodeLink], group_path: typing.List[bpy.types.Node]):
|
||||||
self.shader_node = shader_node
|
self.shader_node = shader_node
|
||||||
self.path = path
|
self.path = path
|
||||||
|
self.group_path = group_path
|
||||||
|
|
||||||
|
|
||||||
# TODO: cache these searches
|
# TODO: cache these searches
|
||||||
def from_socket(start_socket: bpy.types.NodeSocket,
|
def from_socket(start_socket: NodeTreeSearchResult,
|
||||||
shader_node_filter: typing.Union[Filter, typing.Callable]) -> typing.List[NodeTreeSearchResult]:
|
shader_node_filter: typing.Union[Filter, typing.Callable]) -> typing.List[NodeTreeSearchResult]:
|
||||||
"""
|
"""
|
||||||
Find shader nodes where the filter expression is true.
|
Find shader nodes where the filter expression is true.
|
||||||
@ -66,18 +72,39 @@ def from_socket(start_socket: bpy.types.NodeSocket,
|
|||||||
# hide implementation (especially the search path)
|
# hide implementation (especially the search path)
|
||||||
def __search_from_socket(start_socket: bpy.types.NodeSocket,
|
def __search_from_socket(start_socket: bpy.types.NodeSocket,
|
||||||
shader_node_filter: typing.Union[Filter, typing.Callable],
|
shader_node_filter: typing.Union[Filter, typing.Callable],
|
||||||
search_path: typing.List[bpy.types.NodeLink]) -> typing.List[NodeTreeSearchResult]:
|
search_path: typing.List[bpy.types.NodeLink],
|
||||||
|
group_path: typing.List[bpy.types.Node]) -> typing.List[NodeTreeSearchResult]:
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for link in start_socket.links:
|
for link in start_socket.links:
|
||||||
# follow the link to a shader node
|
# follow the link to a shader node
|
||||||
linked_node = link.from_node
|
linked_node = link.from_node
|
||||||
|
|
||||||
|
if linked_node.type == "GROUP":
|
||||||
|
group_output_node = [node for node in linked_node.node_tree.nodes if node.type == "GROUP_OUTPUT"][0]
|
||||||
|
socket = [sock for sock in group_output_node.inputs if sock.name == link.from_socket.name][0]
|
||||||
|
group_path.append(linked_node)
|
||||||
|
linked_results = __search_from_socket(socket, shader_node_filter, search_path + [link], group_path.copy())
|
||||||
|
if linked_results:
|
||||||
|
# add the link to the current path
|
||||||
|
search_path.append(link)
|
||||||
|
results += linked_results
|
||||||
|
continue
|
||||||
|
|
||||||
|
if linked_node.type == "GROUP_INPUT":
|
||||||
|
socket = [sock for sock in group_path[-1].inputs if sock.name == link.from_socket.name][0]
|
||||||
|
linked_results = __search_from_socket(socket, shader_node_filter, search_path + [link], group_path[:-1])
|
||||||
|
if linked_results:
|
||||||
|
# add the link to the current path
|
||||||
|
search_path.append(link)
|
||||||
|
results += linked_results
|
||||||
|
continue
|
||||||
|
|
||||||
# check if the node matches the filter
|
# check if the node matches the filter
|
||||||
if shader_node_filter(linked_node):
|
if shader_node_filter(linked_node):
|
||||||
results.append(NodeTreeSearchResult(linked_node, search_path + [link]))
|
results.append(NodeTreeSearchResult(linked_node, search_path + [link], group_path))
|
||||||
# traverse into inputs of the node
|
# traverse into inputs of the node
|
||||||
for input_socket in linked_node.inputs:
|
for input_socket in linked_node.inputs:
|
||||||
linked_results = __search_from_socket(input_socket, shader_node_filter, search_path + [link])
|
linked_results = __search_from_socket(input_socket, shader_node_filter, search_path + [link], group_path.copy())
|
||||||
if linked_results:
|
if linked_results:
|
||||||
# add the link to the current path
|
# add the link to the current path
|
||||||
search_path.append(link)
|
search_path.append(link)
|
||||||
@ -85,10 +112,330 @@ def from_socket(start_socket: bpy.types.NodeSocket,
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
if start_socket is None:
|
if start_socket.socket is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return __search_from_socket(start_socket, shader_node_filter, [])
|
return __search_from_socket(start_socket.socket, shader_node_filter, [], start_socket.group_path)
|
||||||
|
|
||||||
|
@cached
|
||||||
|
def get_texture_node_from_socket(socket, export_settings):
|
||||||
|
result = from_socket(
|
||||||
|
socket,
|
||||||
|
FilterByType(bpy.types.ShaderNodeTexImage))
|
||||||
|
if not result:
|
||||||
|
return None
|
||||||
|
if result[0].shader_node.image is None:
|
||||||
|
return None
|
||||||
|
return result[0]
|
||||||
|
|
||||||
|
def has_image_node_from_socket(socket, export_settings):
|
||||||
|
result = get_texture_node_from_socket(socket, export_settings)
|
||||||
|
return result is not None
|
||||||
|
|
||||||
|
# return the default value of a socket, even if this socket is linked
|
||||||
|
def get_const_from_default_value_socket(socket, kind):
|
||||||
|
if kind == 'RGB':
|
||||||
|
if socket.socket.type != 'RGBA': return None
|
||||||
|
return list(socket.socket.default_value)[:3]
|
||||||
|
if kind == 'VALUE':
|
||||||
|
if socket.socket.type != 'VALUE': return None
|
||||||
|
return socket.socket.default_value
|
||||||
|
return None
|
||||||
|
|
||||||
|
#TODOSNode : @cached? If yes, need to use id of node tree, has this is probably not fully hashable
|
||||||
|
# For now, not caching it. If we encounter performance issue, we will see later
|
||||||
|
def get_material_nodes(node_tree: bpy.types.NodeTree, group_path, type):
|
||||||
|
"""
|
||||||
|
For a given tree, recursively return all nodes including node groups.
|
||||||
|
"""
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
for node in [n for n in node_tree.nodes if isinstance(n, type) and not n.mute]:
|
||||||
|
nodes.append((node, group_path.copy()))
|
||||||
|
|
||||||
|
# Some weird node groups with missing datablock can have no node_tree, so checking n.node_tree (See #1797)
|
||||||
|
for node in [n for n in node_tree.nodes if n.type == "GROUP" and n.node_tree is not None and not n.mute and n.node_tree.name != get_gltf_old_group_node_name()]: # Do not enter the olf glTF node group
|
||||||
|
new_group_path = group_path.copy()
|
||||||
|
new_group_path.append(node)
|
||||||
|
nodes.extend(get_material_nodes(node.node_tree, new_group_path, type))
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
def get_socket_from_gltf_material_node(blender_material: bpy.types.Material, name: str):
|
||||||
|
"""
|
||||||
|
For a given material input name, retrieve the corresponding node tree socket in the special glTF node group.
|
||||||
|
|
||||||
|
:param blender_material: a blender material for which to get the socket
|
||||||
|
:param name: the name of the socket
|
||||||
|
:return: a blender NodeSocket
|
||||||
|
"""
|
||||||
|
gltf_node_group_names = [get_gltf_node_name().lower(), get_gltf_node_old_name().lower()]
|
||||||
|
if blender_material.node_tree and blender_material.use_nodes:
|
||||||
|
nodes = get_material_nodes(blender_material.node_tree, [blender_material], bpy.types.ShaderNodeGroup)
|
||||||
|
# Some weird node groups with missing datablock can have no node_tree, so checking n.node_tree (See #1797)
|
||||||
|
nodes = [n for n in nodes if n[0].node_tree is not None and ( n[0].node_tree.name.lower().startswith(get_gltf_old_group_node_name()) or n[0].node_tree.name.lower() in gltf_node_group_names)]
|
||||||
|
inputs = sum([[(input, node[1]) for input in node[0].inputs if input.name == name] for node in nodes], [])
|
||||||
|
if inputs:
|
||||||
|
return NodeSocket(inputs[0][0], inputs[0][1])
|
||||||
|
|
||||||
|
return NodeSocket(None, None)
|
||||||
|
|
||||||
|
class NodeSocket:
|
||||||
|
def __init__(self, socket, group_path):
|
||||||
|
self.socket = socket
|
||||||
|
self.group_path = group_path
|
||||||
|
|
||||||
|
class ShNode:
|
||||||
|
def __init__(self, node, group_path):
|
||||||
|
self.node = node
|
||||||
|
self.group_path = group_path
|
||||||
|
|
||||||
|
def get_node_socket(blender_material, type, name):
|
||||||
|
"""
|
||||||
|
For a given material input name, retrieve the corresponding node tree socket for a given node type.
|
||||||
|
|
||||||
|
:param blender_material: a blender material for which to get the socket
|
||||||
|
:return: a blender NodeSocket for a given type
|
||||||
|
"""
|
||||||
|
nodes = get_material_nodes(blender_material.node_tree, [blender_material], type)
|
||||||
|
#TODOSNode : Why checking outputs[0] ? What about alpha for texture node, that is outputs[1] ????
|
||||||
|
nodes = [node for node in nodes if check_if_is_linked_to_active_output(node[0].outputs[0], node[1])]
|
||||||
|
inputs = sum([[(input, node[1]) for input in node[0].inputs if input.name == name] for node in nodes], [])
|
||||||
|
if inputs:
|
||||||
|
return NodeSocket(inputs[0][0], inputs[0][1])
|
||||||
|
return NodeSocket(None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_socket(blender_material: bpy.types.Material, name: str, volume=False):
|
||||||
|
"""
|
||||||
|
For a given material input name, retrieve the corresponding node tree socket.
|
||||||
|
|
||||||
|
:param blender_material: a blender material for which to get the socket
|
||||||
|
:param name: the name of the socket
|
||||||
|
:return: a blender NodeSocket
|
||||||
|
"""
|
||||||
|
if blender_material.node_tree and blender_material.use_nodes:
|
||||||
|
#i = [input for input in blender_material.node_tree.inputs]
|
||||||
|
#o = [output for output in blender_material.node_tree.outputs]
|
||||||
|
if name == "Emissive":
|
||||||
|
# Check for a dedicated Emission node first, it must supersede the newer built-in one
|
||||||
|
# because the newer one is always present in all Principled BSDF materials.
|
||||||
|
emissive_socket = get_node_socket(blender_material, bpy.types.ShaderNodeEmission, "Color")
|
||||||
|
if emissive_socket.socket is not None:
|
||||||
|
return emissive_socket
|
||||||
|
# If a dedicated Emission node was not found, fall back to the Principled BSDF Emission socket.
|
||||||
|
name = "Emission Color"
|
||||||
|
type = bpy.types.ShaderNodeBsdfPrincipled
|
||||||
|
elif name == "Background":
|
||||||
|
type = bpy.types.ShaderNodeBackground
|
||||||
|
name = "Color"
|
||||||
|
else:
|
||||||
|
if volume is False:
|
||||||
|
type = bpy.types.ShaderNodeBsdfPrincipled
|
||||||
|
else:
|
||||||
|
type = bpy.types.ShaderNodeVolumeAbsorption
|
||||||
|
|
||||||
|
return get_node_socket(blender_material, type, name)
|
||||||
|
|
||||||
|
return NodeSocket(None, None)
|
||||||
|
|
||||||
|
def get_factor_from_socket(socket, kind):
|
||||||
|
"""
|
||||||
|
For baseColorFactor, metallicFactor, etc.
|
||||||
|
Get a constant value from a socket, or a constant value
|
||||||
|
from a MULTIPLY node just before the socket.
|
||||||
|
kind is either 'RGB' or 'VALUE'.
|
||||||
|
"""
|
||||||
|
fac = get_const_from_socket(socket, kind)
|
||||||
|
if fac is not None:
|
||||||
|
return fac
|
||||||
|
|
||||||
|
node = previous_node(socket)
|
||||||
|
if node.node is not None:
|
||||||
|
x1, x2 = None, None
|
||||||
|
if kind == 'RGB':
|
||||||
|
if node.node.type == 'MIX' and node.node.data_type == "RGBA" and node.node.blend_type == 'MULTIPLY':
|
||||||
|
# TODO: handle factor in inputs[0]?
|
||||||
|
x1 = get_const_from_socket(NodeSocket(node.node.inputs[6], node.group_path), kind)
|
||||||
|
x2 = get_const_from_socket(NodeSocket(node.node.inputs[7], node.group_path), kind)
|
||||||
|
if kind == 'VALUE':
|
||||||
|
if node.node.type == 'MATH' and node.node.operation == 'MULTIPLY':
|
||||||
|
x1 = get_const_from_socket(NodeSocket(node.node.inputs[0], node.group_path), kind)
|
||||||
|
x2 = get_const_from_socket(NodeSocket(node.node.inputs[1], node.group_path), kind)
|
||||||
|
if x1 is not None and x2 is None: return x1
|
||||||
|
if x2 is not None and x1 is None: return x2
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_const_from_socket(socket, kind):
|
||||||
|
if not socket.socket.is_linked:
|
||||||
|
if kind == 'RGB':
|
||||||
|
if socket.socket.type != 'RGBA': return None
|
||||||
|
return list(socket.socket.default_value)[:3]
|
||||||
|
if kind == 'VALUE':
|
||||||
|
if socket.socket.type != 'VALUE': return None
|
||||||
|
return socket.socket.default_value
|
||||||
|
|
||||||
|
# Handle connection to a constant RGB/Value node
|
||||||
|
prev_node = previous_node(socket)
|
||||||
|
if prev_node.node is not None:
|
||||||
|
if kind == 'RGB' and prev_node.node.type == 'RGB':
|
||||||
|
return list(prev_node.node.outputs[0].default_value)[:3]
|
||||||
|
if kind == 'VALUE' and prev_node.node.type == 'VALUE':
|
||||||
|
return prev_node.node.outputs[0].default_value
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def previous_socket(socket: NodeSocket):
|
||||||
|
soc = socket.socket
|
||||||
|
group_path = socket.group_path.copy()
|
||||||
|
while True:
|
||||||
|
if not soc.is_linked:
|
||||||
|
return NodeSocket(None, None)
|
||||||
|
|
||||||
|
from_socket = soc.links[0].from_socket
|
||||||
|
|
||||||
|
# If we are entering a node group (from outputs)
|
||||||
|
if from_socket.node.type == "GROUP":
|
||||||
|
socket_name = from_socket.name
|
||||||
|
sockets = [n for n in from_socket.node.node_tree.nodes if n.type == "GROUP_OUTPUT"][0].inputs
|
||||||
|
socket = [s for s in sockets if s.name == socket_name][0]
|
||||||
|
group_path.append(from_socket.node)
|
||||||
|
soc = socket
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we are exiting a node group (from inputs)
|
||||||
|
if from_socket.node.type == "GROUP_INPUT":
|
||||||
|
socket_name = from_socket.name
|
||||||
|
sockets = group_path[-1].inputs
|
||||||
|
socket = [s for s in sockets if s.name == socket_name][0]
|
||||||
|
group_path = group_path[:-1]
|
||||||
|
soc = socket
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip over reroute nodes
|
||||||
|
if from_socket.node.type == 'REROUTE':
|
||||||
|
soc = from_socket.node.inputs[0]
|
||||||
|
continue
|
||||||
|
|
||||||
|
return NodeSocket(from_socket, group_path)
|
||||||
|
|
||||||
|
|
||||||
|
def previous_node(socket: NodeSocket):
|
||||||
|
prev_socket = previous_socket(socket)
|
||||||
|
if prev_socket.socket is not None:
|
||||||
|
return ShNode(prev_socket.socket.node, prev_socket.group_path)
|
||||||
|
return ShNode(None, None)
|
||||||
|
|
||||||
|
def get_texture_transform_from_mapping_node(mapping_node):
|
||||||
|
if mapping_node.node.vector_type not in ["TEXTURE", "POINT", "VECTOR"]:
|
||||||
|
gltf2_io_debug.print_console("WARNING",
|
||||||
|
"Skipping exporting texture transform because it had type " +
|
||||||
|
mapping_node.node.vector_type + "; recommend using POINT instead"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
rotation_0, rotation_1 = mapping_node.node.inputs['Rotation'].default_value[0], mapping_node.node.inputs['Rotation'].default_value[1]
|
||||||
|
if rotation_0 or rotation_1:
|
||||||
|
# TODO: can we handle this?
|
||||||
|
gltf2_io_debug.print_console("WARNING",
|
||||||
|
"Skipping exporting texture transform because it had non-zero "
|
||||||
|
"rotations in the X/Y direction; only a Z rotation can be exported!"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
mapping_transform = {}
|
||||||
|
mapping_transform["offset"] = [mapping_node.node.inputs['Location'].default_value[0], mapping_node.node.inputs['Location'].default_value[1]]
|
||||||
|
mapping_transform["rotation"] = mapping_node.node.inputs['Rotation'].default_value[2]
|
||||||
|
mapping_transform["scale"] = [mapping_node.node.inputs['Scale'].default_value[0], mapping_node.node.inputs['Scale'].default_value[1]]
|
||||||
|
|
||||||
|
if mapping_node.node.vector_type == "TEXTURE":
|
||||||
|
# This means use the inverse of the TRS transform.
|
||||||
|
def inverted(mapping_transform):
|
||||||
|
offset = mapping_transform["offset"]
|
||||||
|
rotation = mapping_transform["rotation"]
|
||||||
|
scale = mapping_transform["scale"]
|
||||||
|
|
||||||
|
# Inverse of a TRS is not always a TRS. This function will be right
|
||||||
|
# at least when the following don't occur.
|
||||||
|
if abs(rotation) > 1e-5 and abs(scale[0] - scale[1]) > 1e-5:
|
||||||
|
return None
|
||||||
|
if abs(scale[0]) < 1e-5 or abs(scale[1]) < 1e-5:
|
||||||
|
return None
|
||||||
|
|
||||||
|
new_offset = Matrix.Rotation(-rotation, 3, 'Z') @ Vector((-offset[0], -offset[1], 1))
|
||||||
|
new_offset[0] /= scale[0]; new_offset[1] /= scale[1]
|
||||||
|
return {
|
||||||
|
"offset": new_offset[0:2],
|
||||||
|
"rotation": -rotation,
|
||||||
|
"scale": [1/scale[0], 1/scale[1]],
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping_transform = inverted(mapping_transform)
|
||||||
|
if mapping_transform is None:
|
||||||
|
gltf2_io_debug.print_console("WARNING",
|
||||||
|
"Skipping exporting texture transform with type TEXTURE because "
|
||||||
|
"we couldn't convert it to TRS; recommend using POINT instead"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
elif mapping_node.node.vector_type == "VECTOR":
|
||||||
|
# Vectors don't get translated
|
||||||
|
mapping_transform["offset"] = [0, 0]
|
||||||
|
|
||||||
|
texture_transform = texture_transform_blender_to_gltf(mapping_transform)
|
||||||
|
|
||||||
|
if all([component == 0 for component in texture_transform["offset"]]):
|
||||||
|
del(texture_transform["offset"])
|
||||||
|
if all([component == 1 for component in texture_transform["scale"]]):
|
||||||
|
del(texture_transform["scale"])
|
||||||
|
if texture_transform["rotation"] == 0:
|
||||||
|
del(texture_transform["rotation"])
|
||||||
|
|
||||||
|
if len(texture_transform) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return texture_transform
|
||||||
|
|
||||||
|
def check_if_is_linked_to_active_output(shader_socket, group_path):
|
||||||
|
for link in shader_socket.links:
|
||||||
|
|
||||||
|
# If we are entering a node group
|
||||||
|
if link.to_node.type == "GROUP":
|
||||||
|
socket_name = link.to_socket.name
|
||||||
|
sockets = [n for n in link.to_node.node_tree.nodes if n.type == "GROUP_INPUT"][0].outputs
|
||||||
|
socket = [s for s in sockets if s.name == socket_name][0]
|
||||||
|
group_path.append(link.to_node)
|
||||||
|
#TODOSNode : Why checking outputs[0] ? What about alpha for texture node, that is outputs[1] ????
|
||||||
|
ret = check_if_is_linked_to_active_output(socket, group_path) # recursive until find an output material node
|
||||||
|
if ret is True:
|
||||||
|
return True
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we are exiting a node group
|
||||||
|
if link.to_node.type == "GROUP_OUTPUT":
|
||||||
|
socket_name = link.to_socket.name
|
||||||
|
sockets = group_path[-1].outputs
|
||||||
|
socket = [s for s in sockets if s.name == socket_name][0]
|
||||||
|
group_path = group_path[:-1]
|
||||||
|
#TODOSNode : Why checking outputs[0] ? What about alpha for texture node, that is outputs[1] ????
|
||||||
|
ret = check_if_is_linked_to_active_output(socket, group_path) # recursive until find an output material node
|
||||||
|
if ret is True:
|
||||||
|
return True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(link.to_node, bpy.types.ShaderNodeOutputMaterial) and link.to_node.is_active_output is True:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if len(link.to_node.outputs) > 0: # ignore non active output, not having output sockets
|
||||||
|
#TODOSNode : Why checking outputs[0] ? What about alpha for texture node, that is outputs[1] ????
|
||||||
|
ret = check_if_is_linked_to_active_output(link.to_node.outputs[0], group_path) # recursive until find an output material node
|
||||||
|
if ret is True:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def get_vertex_color_info(primary_socket, sockets, export_settings):
|
def get_vertex_color_info(primary_socket, sockets, export_settings):
|
||||||
return {"color": None, "alpha": None} #TODO, placeholder for now
|
return {"color": None, "alpha": None} #TODO, placeholder for now
|
||||||
|
@ -39,7 +39,7 @@ def specular(mh, location_specular,
|
|||||||
x_specularcolor, y_specularcolor = location_specular_tint
|
x_specularcolor, y_specularcolor = location_specular_tint
|
||||||
|
|
||||||
if tex_specular_info is None:
|
if tex_specular_info is None:
|
||||||
specular_socket.default_value = specular_factor
|
specular_socket.default_value = specular_factor / 2.0
|
||||||
else:
|
else:
|
||||||
# Mix specular factor
|
# Mix specular factor
|
||||||
if specular_factor != 1.0:
|
if specular_factor != 1.0:
|
||||||
@ -51,7 +51,7 @@ def specular(mh, location_specular,
|
|||||||
mh.node_tree.links.new(specular_socket, node.outputs[0])
|
mh.node_tree.links.new(specular_socket, node.outputs[0])
|
||||||
# Inputs
|
# Inputs
|
||||||
specular_socket = node.inputs[0]
|
specular_socket = node.inputs[0]
|
||||||
node.inputs[1].default_value = specular_factor
|
node.inputs[1].default_value = specular_factor / 2.0
|
||||||
x_specular -= 200
|
x_specular -= 200
|
||||||
|
|
||||||
texture(
|
texture(
|
||||||
|
@ -135,6 +135,7 @@ class BlenderNode():
|
|||||||
bpy.data.collections.new(BLENDER_GLTF_SPECIAL_COLLECTION)
|
bpy.data.collections.new(BLENDER_GLTF_SPECIAL_COLLECTION)
|
||||||
bpy.data.scenes[gltf.blender_scene].collection.children.link(bpy.data.collections[BLENDER_GLTF_SPECIAL_COLLECTION])
|
bpy.data.scenes[gltf.blender_scene].collection.children.link(bpy.data.collections[BLENDER_GLTF_SPECIAL_COLLECTION])
|
||||||
bpy.data.collections[BLENDER_GLTF_SPECIAL_COLLECTION].hide_viewport = True
|
bpy.data.collections[BLENDER_GLTF_SPECIAL_COLLECTION].hide_viewport = True
|
||||||
|
bpy.data.collections[BLENDER_GLTF_SPECIAL_COLLECTION].hide_render = True
|
||||||
|
|
||||||
# Create an icosphere, and assign it to the collection
|
# Create an icosphere, and assign it to the collection
|
||||||
bpy.ops.mesh.primitive_ico_sphere_add(radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
bpy.ops.mesh.primitive_ico_sphere_add(radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||||
@ -187,7 +188,10 @@ class BlenderNode():
|
|||||||
arma_mat = vnode.editbone_arma_mat
|
arma_mat = vnode.editbone_arma_mat
|
||||||
editbone.head = arma_mat @ Vector((0, 0, 0))
|
editbone.head = arma_mat @ Vector((0, 0, 0))
|
||||||
editbone.tail = arma_mat @ Vector((0, 1, 0))
|
editbone.tail = arma_mat @ Vector((0, 1, 0))
|
||||||
editbone.length = vnode.bone_length
|
if gltf.import_settings['bone_heuristic'] == "BLENDER":
|
||||||
|
editbone.length = vnode.bone_length / max(blender_arma.scale)
|
||||||
|
else:
|
||||||
|
editbone.length = vnode.bone_length
|
||||||
editbone.align_roll(arma_mat @ Vector((0, 0, 1)) - editbone.head)
|
editbone.align_roll(arma_mat @ Vector((0, 0, 1)) - editbone.head)
|
||||||
|
|
||||||
if isinstance(id, int):
|
if isinstance(id, int):
|
||||||
@ -225,7 +229,8 @@ class BlenderNode():
|
|||||||
|
|
||||||
if gltf.import_settings['bone_heuristic'] == "BLENDER":
|
if gltf.import_settings['bone_heuristic'] == "BLENDER":
|
||||||
pose_bone.custom_shape = bpy.data.objects[gltf.bone_shape]
|
pose_bone.custom_shape = bpy.data.objects[gltf.bone_shape]
|
||||||
pose_bone.custom_shape_scale_xyz = Vector([0.1, 0.1, 0.1])
|
armature_max_dim = max([blender_arma.dimensions[0] / blender_arma.scale[0], blender_arma.dimensions[1] / blender_arma.scale[1], blender_arma.dimensions[2] / blender_arma.scale[2]])
|
||||||
|
pose_bone.custom_shape_scale_xyz = Vector([armature_max_dim * 0.2] * 3)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_mesh_object(gltf, vnode):
|
def create_mesh_object(gltf, vnode):
|
||||||
|
@ -50,9 +50,6 @@ def pbr_metallic_roughness(mh: MaterialHelper):
|
|||||||
# This value may be overridden later if IOR extension is set on file
|
# This value may be overridden later if IOR extension is set on file
|
||||||
pbr_node.inputs['IOR'].default_value = GLTF_IOR
|
pbr_node.inputs['IOR'].default_value = GLTF_IOR
|
||||||
|
|
||||||
pbr_node.inputs['Specular IOR Level'].default_value = 0.0 # Will be overridden by KHR_materials_specular if set
|
|
||||||
pbr_node.inputs['Specular Tint'].default_value = [0.0]*3 + [1.0] # Will be overridden by KHR_materials_specular if set
|
|
||||||
|
|
||||||
if mh.pymat.occlusion_texture is not None:
|
if mh.pymat.occlusion_texture is not None:
|
||||||
if mh.settings_node is None:
|
if mh.settings_node is None:
|
||||||
mh.settings_node = make_settings_node(mh)
|
mh.settings_node = make_settings_node(mh)
|
||||||
|
@ -41,7 +41,7 @@ def texture(
|
|||||||
if forced_image is None:
|
if forced_image is None:
|
||||||
|
|
||||||
if mh.gltf.import_settings['import_webp_texture'] is True:
|
if mh.gltf.import_settings['import_webp_texture'] is True:
|
||||||
# Get the webp image if there is one
|
# Get the WebP image if there is one
|
||||||
if pytexture.extensions \
|
if pytexture.extensions \
|
||||||
and 'EXT_texture_webp' in pytexture.extensions \
|
and 'EXT_texture_webp' in pytexture.extensions \
|
||||||
and pytexture.extensions['EXT_texture_webp']['source'] is not None:
|
and pytexture.extensions['EXT_texture_webp']['source'] is not None:
|
||||||
|
@ -47,8 +47,5 @@ class Buffer:
|
|||||||
def to_bytes(self):
|
def to_bytes(self):
|
||||||
return self.__data
|
return self.__data
|
||||||
|
|
||||||
def to_embed_string(self):
|
|
||||||
return 'data:application/octet-stream;base64,' + base64.b64encode(self.__data).decode('ascii')
|
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.__data = b""
|
self.__data = b""
|
||||||
|
@ -539,7 +539,7 @@ class NWPreviewNode(Operator, NWBase):
|
|||||||
|
|
||||||
if not viewer_socket:
|
if not viewer_socket:
|
||||||
# create viewer socket
|
# create viewer socket
|
||||||
viewer_socket = node.node_tree.interface.new_socket(viewer_socket_name, in_out={'OUTPUT'}, socket_type=socket_type)
|
viewer_socket = node.node_tree.interface.new_socket(viewer_socket_name, in_out='OUTPUT', socket_type=socket_type)
|
||||||
viewer_socket.NWViewerSocket = True
|
viewer_socket.NWViewerSocket = True
|
||||||
return viewer_socket
|
return viewer_socket
|
||||||
|
|
||||||
|
@ -89,10 +89,10 @@ class ActionSlot(PropertyGroup, ActionSlotBase):
|
|||||||
|
|
||||||
target_space: EnumProperty(
|
target_space: EnumProperty(
|
||||||
name="Transform Space",
|
name="Transform Space",
|
||||||
items=[("WORLD", "World Space", "World Space"),
|
items=[("WORLD", "World Space", "World Space", 0),
|
||||||
("POSE", "Pose Space", "Pose Space"),
|
# ("POSE", "Pose Space", "Pose Space", 1),
|
||||||
("LOCAL_WITH_PARENT", "Local With Parent", "Local With Parent"),
|
# ("LOCAL_WITH_PARENT", "Local With Parent", "Local With Parent", 2),
|
||||||
("LOCAL", "Local Space", "Local Space")],
|
("LOCAL", "Local Space", "Local Space", 3)],
|
||||||
default="LOCAL"
|
default="LOCAL"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -153,9 +153,10 @@ def pVisRotExec(bone, active, context):
|
|||||||
|
|
||||||
def pVisScaExec(bone, active, context):
|
def pVisScaExec(bone, active, context):
|
||||||
obj_bone = bone.id_data
|
obj_bone = bone.id_data
|
||||||
bone.scale = getmat(bone, active, context,
|
bone.scale = getmat(
|
||||||
not obj_bone.data.bones[bone.name].use_inherit_scale)\
|
bone, active, context,
|
||||||
.to_scale()
|
obj_bone.data.bones[bone.name].inherit_scale not in {'NONE', 'NONE_LEGACY'}
|
||||||
|
).to_scale()
|
||||||
|
|
||||||
|
|
||||||
def pDrwExec(bone, active, context):
|
def pDrwExec(bone, active, context):
|
||||||
|
@ -156,12 +156,12 @@ def draw_callback_view():
|
|||||||
|
|
||||||
if data_euler or data_quat:
|
if data_euler or data_quat:
|
||||||
cursor = bpy.context.scene.cursor.location.copy()
|
cursor = bpy.context.scene.cursor.location.copy()
|
||||||
derived_matrices = []
|
derived_matrices = dict()
|
||||||
for key, quat in data_quat.values():
|
for key, quat in data_quat.items():
|
||||||
matrix = quat.to_matrix().to_4x4()
|
matrix = quat.to_matrix().to_4x4()
|
||||||
matrix.translation = cursor
|
matrix.translation = cursor
|
||||||
derived_matrices[key] = matrix
|
derived_matrices[key] = matrix
|
||||||
for key, eul in data_euler.values():
|
for key, eul in data_euler.items():
|
||||||
matrix = eul.to_matrix().to_4x4()
|
matrix = eul.to_matrix().to_4x4()
|
||||||
matrix.translation = cursor
|
matrix.translation = cursor
|
||||||
derived_matrices[key] = matrix
|
derived_matrices[key] = matrix
|
||||||
|
@ -99,9 +99,6 @@ class VIEW3D_MT_Pose(Menu):
|
|||||||
layout.operator("pose.quaternions_flip")
|
layout.operator("pose.quaternions_flip")
|
||||||
layout.operator_context = 'INVOKE_AREA'
|
layout.operator_context = 'INVOKE_AREA'
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator("armature.armature_layers", text="Change Armature Layers...")
|
|
||||||
layout.operator("pose.bone_layers", text="Change Bone Layers...")
|
|
||||||
layout.separator()
|
|
||||||
layout.menu("VIEW3D_MT_pose_showhide")
|
layout.menu("VIEW3D_MT_pose_showhide")
|
||||||
layout.menu("VIEW3D_MT_bone_options_toggle", text="Bone Settings")
|
layout.menu("VIEW3D_MT_bone_options_toggle", text="Bone Settings")
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import FloatProperty, FloatVectorProperty
|
from bpy.props import FloatProperty, FloatVectorProperty
|
||||||
|
from bpy.app.translations import pgettext_iface as iface_
|
||||||
import gpu
|
import gpu
|
||||||
from gpu_extras.batch import batch_for_shader
|
from gpu_extras.batch import batch_for_shader
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
@ -248,8 +249,8 @@ class SUNPOS_OT_ShowHdr(bpy.types.Operator):
|
|||||||
self.initial_azimuth = context.scene.sun_pos_properties.hdr_azimuth
|
self.initial_azimuth = context.scene.sun_pos_properties.hdr_azimuth
|
||||||
|
|
||||||
context.workspace.status_text_set(
|
context.workspace.status_text_set(
|
||||||
"Enter/LMB: confirm, Esc/RMB: cancel,"
|
iface_("Enter/LMB: confirm, Esc/RMB: cancel, MMB: pan, "
|
||||||
" MMB: pan, mouse wheel: zoom, Ctrl + mouse wheel: set exposure")
|
"mouse wheel: zoom, Ctrl + mouse wheel: set exposure"))
|
||||||
|
|
||||||
self._handle = bpy.types.SpaceView3D.draw_handler_add(
|
self._handle = bpy.types.SpaceView3D.draw_handler_add(
|
||||||
draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL'
|
draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL'
|
||||||
|
@ -416,6 +416,19 @@ translations_tuple = (
|
|||||||
("fr_FR", "Année",
|
("fr_FR", "Année",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
|
(("*", "Unknown projection"),
|
||||||
|
(("scripts/addons/sun_position/hdr.py:181",),
|
||||||
|
()),
|
||||||
|
("fr_FR", "Projection inconnue",
|
||||||
|
(False, ())),
|
||||||
|
),
|
||||||
|
(("*", "Enter/LMB: confirm, Esc/RMB: cancel, MMB: pan, mouse wheel: zoom, Ctrl + mouse wheel: set exposure"),
|
||||||
|
(("scripts/addons/sun_position/hdr.py:252",),
|
||||||
|
()),
|
||||||
|
("fr_FR", "Entrée/ClicG : Confirmer, Échap/ClicD : Annuler, ClicM : défiler, "
|
||||||
|
"molette : zoom, Ctrl + molette : exposition",
|
||||||
|
(False, ())),
|
||||||
|
),
|
||||||
(("*", "Could not find 3D View"),
|
(("*", "Could not find 3D View"),
|
||||||
(("scripts/addons/sun_position/hdr.py:263",),
|
(("scripts/addons/sun_position/hdr.py:263",),
|
||||||
()),
|
()),
|
||||||
@ -428,12 +441,6 @@ translations_tuple = (
|
|||||||
("fr_FR", "Veuillez utiliser un nœud de texture d’environnement",
|
("fr_FR", "Veuillez utiliser un nœud de texture d’environnement",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
(("*", "Unknown projection"),
|
|
||||||
(("scripts/addons/sun_position/hdr.py:181",),
|
|
||||||
()),
|
|
||||||
("fr_FR", "Projection inconnue",
|
|
||||||
(False, ())),
|
|
||||||
),
|
|
||||||
(("*", "Show options and info:"),
|
(("*", "Show options and info:"),
|
||||||
(("scripts/addons/sun_position/properties.py:297",),
|
(("scripts/addons/sun_position/properties.py:297",),
|
||||||
()),
|
()),
|
||||||
|
@ -344,7 +344,9 @@ class UI_OT_i18n_addon_translation_export(Operator):
|
|||||||
if not lng.use:
|
if not lng.use:
|
||||||
print("Skipping {} language ({}).".format(lng.name, lng.uid))
|
print("Skipping {} language ({}).".format(lng.name, lng.uid))
|
||||||
continue
|
continue
|
||||||
uid = utils_i18n.find_best_isocode_matches(lng.uid, trans.trans.keys())
|
translation_keys = {k for k in trans.trans.keys()
|
||||||
|
if k != self.settings.PARSER_TEMPLATE_ID}
|
||||||
|
uid = utils_i18n.find_best_isocode_matches(lng.uid, translation_keys)
|
||||||
if uid:
|
if uid:
|
||||||
uids.append(uid[0])
|
uids.append(uid[0])
|
||||||
|
|
||||||
@ -357,8 +359,8 @@ class UI_OT_i18n_addon_translation_export(Operator):
|
|||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
continue
|
continue
|
||||||
msgs = utils_i18n.I18nMessages(kind='PO', src=path, settings=self.settings)
|
msgs = utils_i18n.I18nMessages(kind='PO', src=path, settings=self.settings)
|
||||||
msgs.update(trans.msgs[self.settings.PARSER_TEMPLATE_ID])
|
msgs.update(trans.trans[self.settings.PARSER_TEMPLATE_ID])
|
||||||
trans.msgs[uid] = msgs
|
trans.trans[uid] = msgs
|
||||||
|
|
||||||
trans.write(kind='PO', langs=set(uids))
|
trans.write(kind='PO', langs=set(uids))
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user