diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 804d0159b70d31f7095f1e8fcee8bc24d8639d6f..4a5344d84608316d1fc999b6624cabe1b9ac54f1 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -18,6 +18,7 @@ import os import bpy +import datetime from bpy_extras.io_utils import ImportHelper, ExportHelper from bpy.types import Operator, AddonPreferences @@ -158,11 +159,11 @@ class ExportGLTF2_Base: default=False ) - export_layers: BoolProperty( - name='Export all layers', - description='', - default=True - ) + # export_layers: BoolProperty( + # name='Export all layers', + # description='', + # default=True + # ) export_extras: BoolProperty( name='Export extras', @@ -232,6 +233,12 @@ class ExportGLTF2_Base: default=False ) + export_all_influences: BoolProperty( + name='Export all bone influences', + description='Export more than four joint vertex influences', + default=False + ) + export_morph: BoolProperty( name='Export morphing', description='', @@ -307,6 +314,8 @@ class ExportGLTF2_Base: # All custom export settings are stored in this container. export_settings = {} + export_settings['timestamp'] = datetime.datetime.now() + export_settings['gltf_filepath'] = bpy.path.ensure_ext(self.filepath, self.filename_ext) export_settings['gltf_filedirectory'] = os.path.dirname(export_settings['gltf_filepath']) + '/' @@ -328,7 +337,7 @@ class ExportGLTF2_Base: else: export_settings['gltf_camera_infinite'] = False export_settings['gltf_selected'] = self.export_selected - export_settings['gltf_layers'] = self.export_layers + export_settings['gltf_layers'] = True #self.export_layers export_settings['gltf_extras'] = self.export_extras export_settings['gltf_yup'] = self.export_yup export_settings['gltf_apply'] = self.export_apply @@ -346,8 +355,10 @@ class ExportGLTF2_Base: export_settings['gltf_skins'] = self.export_skins if self.export_skins: export_settings['gltf_bake_skins'] = self.export_bake_skins + export_settings['gltf_all_vertex_influences'] = self.export_all_influences else: export_settings['gltf_bake_skins'] = False + export_settings['gltf_all_vertex_influences'] = False export_settings['gltf_frame_step'] = self.export_frame_step export_settings['gltf_morph'] = self.export_morph if self.export_morph: @@ -384,7 +395,7 @@ class ExportGLTF2_Base: col = layout.box().column() col.label(text='Nodes:') # , icon='OOPS') col.prop(self, 'export_selected') - col.prop(self, 'export_layers') + #col.prop(self, 'export_layers') col.prop(self, 'export_extras') col.prop(self, 'export_yup') @@ -413,6 +424,10 @@ class ExportGLTF2_Base: col.prop(self, 'export_materials') col.prop(self, 'export_texture_transform') + col = layout.box().column() + col.label(text='Lights:') # , icon='LIGHT_DATA') + col.prop(self, 'export_lights') + col = layout.box().column() col.label(text='Animation:') # , icon='OUTLINER_DATA_POSE') col.prop(self, 'export_animations') @@ -426,19 +441,13 @@ class ExportGLTF2_Base: col.prop(self, 'export_skins') if self.export_skins: col.prop(self, 'export_bake_skins') + col.prop(self, 'export_all_influences') col.prop(self, 'export_morph') if self.export_morph: col.prop(self, 'export_morph_normal') if self.export_morph_normal: col.prop(self, 'export_morph_tangent') - addon_prefs = context.user_preferences.addons[__name__].preferences - if addon_prefs.experimental: - col = layout.box().column() - col.label(text='Experimental:') # , icon='RADIO') - col.prop(self, 'export_lights') - col.prop(self, 'export_displacement') - row = layout.row() row.operator( GLTF2ExportSettings.bl_idname, @@ -474,16 +483,6 @@ def menu_func_export(self, context): self.layout.operator(ExportGLTF2_GLB.bl_idname, text='Binary glTF 2.0 (.glb)') -class ExportGLTF2_AddonPreferences(AddonPreferences): - bl_idname = __name__ - - experimental: BoolProperty(name='Enable experimental glTF export settings', default=False) - - def draw(self, context): - layout = self.layout - layout.prop(self, "experimental") - - class ImportglTF2(Operator, ImportHelper): bl_idname = 'import_scene.gltf' bl_label = "glTF 2.0 (.gltf/.glb)" @@ -549,7 +548,6 @@ classes = ( GLTF2ExportSettings, ExportGLTF2_GLTF, ExportGLTF2_GLB, - ExportGLTF2_AddonPreferences, ImportglTF2 ) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py index 4941ffe2912829167beb15a18d322603044cf3c6..b89f51d700c4b1ca85a86ac0cb1a8a73267cea16 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py @@ -98,7 +98,7 @@ def __get_image_data(sockets_or_slots): # For shared ressources, 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 # ressources. - def split_pixels_by_channels(image: bpy.types.Image) -> typing.Iterable[typing.Iterable[float]]: + def split_pixels_by_channels(image: bpy.types.Image) -> typing.List[typing.List[float]]: pixels = np.array(image.pixels) pixels = pixels.reshape((pixels.shape[0] // image.channels, image.channels)) channels = np.split(pixels, pixels.shape[1], axis=1) @@ -139,7 +139,7 @@ def __get_image_data(sockets_or_slots): return image elif __is_slot(sockets_or_slots): texture = __get_tex_from_slot(sockets_or_slots[0]) - pixels = texture.image.pixels + pixels = split_pixels_by_channels(texture.image) image_data = gltf2_io_image_data.ImageData( texture.name, diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_light_spots.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_light_spots.py new file mode 100644 index 0000000000000000000000000000000000000000..6b1f95e2c0e906adc247123b8e567b08975368bc --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_light_spots.py @@ -0,0 +1,50 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from typing import Optional, List, Any + +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached + +from io_scene_gltf2.io.com import gltf2_io_lights_punctual +from io_scene_gltf2.io.com import gltf2_io_debug + + +def gather_light_spot(blender_lamp, export_settings) -> Optional[gltf2_io_lights_punctual.LightSpot]: + + if not __filter_light_spot(blender_lamp, export_settings): + return None + + spot = gltf2_io_lights_punctual.LightSpot( + inner_cone_angle=__gather_inner_cone_angle(blender_lamp, export_settings), + outer_cone_angle=__gather_outer_cone_angle(blender_lamp, export_settings) + ) + return spot + + +def __filter_light_spot(blender_lamp, _) -> bool: + if blender_lamp.type != "SPOT": + return False + + return True + + +def __gather_inner_cone_angle(blender_lamp, _) -> Optional[float]: + angle = blender_lamp.spot_size * 0.5 + return angle - angle * blender_lamp.spot_blend + + +def __gather_outer_cone_angle(blender_lamp, _) -> Optional[float]: + return blender_lamp.spot_size * 0.5 + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_lights.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_lights.py new file mode 100644 index 0000000000000000000000000000000000000000..5cc3dc62ba9c887970e8369e3d158844af750473 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_lights.py @@ -0,0 +1,131 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import math +from typing import Optional, List, Dict, Any + +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached + +from io_scene_gltf2.io.com import gltf2_io_lights_punctual +from io_scene_gltf2.io.com import gltf2_io_debug + +from io_scene_gltf2.blender.exp import gltf2_blender_gather_light_spots +from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree + +@cached +def gather_lights_punctual(blender_lamp, export_settings) -> Optional[Dict[str, Any]]: + if not __filter_lights_punctual(blender_lamp, export_settings): + return None + + light = gltf2_io_lights_punctual.Light( + color=__gather_color(blender_lamp, export_settings), + intensity=__gather_intensity(blender_lamp, export_settings), + spot=__gather_spot(blender_lamp, export_settings), + type=__gather_type(blender_lamp, export_settings), + range=__gather_range(blender_lamp, export_settings), + name=__gather_name(blender_lamp, export_settings), + extensions=__gather_extensions(blender_lamp, export_settings), + extras=__gather_extras(blender_lamp, export_settings) + ) + + return light.to_dict() + + +def __filter_lights_punctual(blender_lamp, export_settings) -> bool: + if blender_lamp.type in ["HEMI", "AREA"]: + gltf2_io_debug.print_console("WARNING", "Unsupported light source {}".format(blender_lamp.type)) + return False + + return True + + +def __gather_color(blender_lamp, export_settings) -> Optional[List[float]]: + emission_node = __get_cycles_emission_node(blender_lamp) + if emission_node is not None: + return emission_node.inputs["Color"].default_value + + return list(blender_lamp.color) + + +def __gather_intensity(blender_lamp, _) -> Optional[float]: + emission_node = __get_cycles_emission_node(blender_lamp) + if emission_node is not None: + if blender_lamp.type != 'SUN': + # When using cycles, the strength should be influenced by a LightFalloff node + result = gltf2_blender_search_node_tree.from_socket( + emission_node.get("Strength"), + gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeLightFalloff) + ) + if result: + quadratic_falloff_node = result[0].shader_node + emission_strength = quadratic_falloff_node.inputs["Strength"].default_value / (math.pi * 4.0) + else: + gltf2_io_debug.print_console('WARNING', + 'No quadratic light falloff node attached to emission strength property') + emission_strength = blender_lamp.energy + else: + emission_strength = emission_node.inputs["Strength"].default_value + return emission_strength + + return blender_lamp.energy + + +def __gather_spot(blender_lamp, export_settings) -> Optional[gltf2_io_lights_punctual.LightSpot]: + if blender_lamp.type == "SPOT": + return gltf2_blender_gather_light_spots.gather_light_spot(blender_lamp, export_settings) + return None + + +def __gather_type(blender_lamp, _) -> str: + return { + "POINT": "point", + "SUN": "directional", + "SPOT": "spot" + }[blender_lamp.type] + + +def __gather_range(blender_lamp, export_settings) -> Optional[float]: + # TODO: calculate range from + # https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual#range-property + return None + + +def __gather_name(blender_lamp, export_settings) -> Optional[str]: + return blender_lamp.name + + +def __gather_extensions(blender_lamp, export_settings) -> Optional[dict]: + return None + + +def __gather_extras(blender_lamp, export_settings) -> Optional[Any]: + return None + + +def __get_cycles_emission_node(blender_lamp) -> Optional[bpy.types.ShaderNodeEmission]: + if blender_lamp.use_nodes and blender_lamp.node_tree: + for currentNode in blender_lamp.node_tree.nodes: + if isinstance(currentNode, bpy.types.ShaderNodeOutputLamp): + if not currentNode.is_active_output: + continue + result = gltf2_blender_search_node_tree.from_socket( + currentNode.inputs.get("Surface"), + gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeEmission) + ) + if not result: + continue + return result[0].shader_node + return None + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py index 56a0a707aedaab8d8e0e744949e79d42b4751c46..e8da100d335e9c50a6c8e29732993c14917e0424 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import bpy + from . import gltf2_blender_export_keys from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins @@ -19,7 +21,9 @@ from io_scene_gltf2.blender.exp import gltf2_blender_gather_cameras from io_scene_gltf2.blender.exp import gltf2_blender_gather_mesh from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints from io_scene_gltf2.blender.exp import gltf2_blender_extract +from io_scene_gltf2.blender.exp import gltf2_blender_gather_lights from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.io.com import gltf2_io_extensions @cached @@ -87,7 +91,28 @@ def __gather_children(blender_object, export_settings): def __gather_extensions(blender_object, export_settings): - return None + extensions = {} + + if export_settings["gltf_lights"] and (blender_object.type == "LAMP" or blender_object.type == "LIGHT"): + blender_lamp = blender_object.data + light = gltf2_blender_gather_lights.gather_lights_punctual( + blender_lamp, + export_settings + ) + if light is not None: + light_extension = gltf2_io_extensions.ChildOfRootExtension( + name="KHR_lights_punctual", + path=["lights"], + extension=light + ) + extensions["KHR_lights_punctual"] = gltf2_io_extensions.Extension( + name="KHR_lights_punctual", + extension={ + "light": light_extension + } + ) + + return extensions if extensions else None def __gather_extras(blender_object, export_settings): diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py index bd1294cd7407356db5a334f186f82b11bf8cb685..76a1f0e4a24fc71ae7cae73d8d47f7e6023da800 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py @@ -168,8 +168,8 @@ def __gather_skins(blender_primitive, export_settings): if bone_index >= 4: gltf2_io_debug.print_console("WARNING", "There are more than 4 joint vertex influences." "Consider to apply blenders Limit Total function.") - # TODO: add option to stop after 4 - # break + if not export_settings['gltf_all_vertex_influences']: + break # joints internal_joint = blender_primitive["attributes"][joint_id] diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py index 14b62c230f89efa70b05a71ba3fd89366edbf4a4..919e3d3ae310184a689284c273cec33c366a7c49 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py @@ -12,7 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional, List, Dict + from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.io.com import gltf2_io_extensions from io_scene_gltf2.io.exp import gltf2_io_binary_data from io_scene_gltf2.io.exp import gltf2_io_image_data from io_scene_gltf2.io.exp import gltf2_io_buffer @@ -208,6 +211,20 @@ class GlTF2Exporter: # TODO: allow embedding of images (base64) return image.name + ".png" + @classmethod + def __get_key_path(cls, d: dict, keypath: List[str], default=[]): + """Create if necessary and get the element at key path from a dict""" + key = keypath.pop(0) + + if len(keypath) == 0: + v = d.get(key, default) + d[key] = v + return v + + d_key = d.get(key, {}) + d[key] = d_key + return cls.__get_key_path(d[key], keypath, default) + def __traverse(self, node): """ Recursively traverse a scene graph consisting of gltf compatible elements. @@ -215,21 +232,21 @@ class GlTF2Exporter: The tree is traversed downwards until a primitive is reached. Then any ChildOfRoot property is stored in the according list in the glTF and replaced with a index reference in the upper level. """ - def traverse_all_members(node): + def __traverse_property(node): for member_name in [a for a in dir(node) if not a.startswith('__') and not callable(getattr(node, a))]: new_value = self.__traverse(getattr(node, member_name)) setattr(node, member_name, new_value) # usually this is the same as before - # TODO: maybe with extensions hooks we can find a more elegant solution - if member_name == "extensions" and new_value is not None: - for extension_name in new_value.keys(): - self.__append_unique_and_get_index(self.__gltf.extensions_used, extension_name) - self.__append_unique_and_get_index(self.__gltf.extensions_required, extension_name) + # # TODO: maybe with extensions hooks we can find a more elegant solution + # if member_name == "extensions" and new_value is not None: + # for extension_name in new_value.keys(): + # self.__append_unique_and_get_index(self.__gltf.extensions_used, extension_name) + # self.__append_unique_and_get_index(self.__gltf.extensions_required, extension_name) return node # traverse nodes of a child of root property type and add them to the glTF root if type(node) in self.__childOfRootPropertyTypeLookup: - node = traverse_all_members(node) + node = __traverse_property(node) idx = self.__to_reference(node) # child of root properties are only present at root level --> replace with index in upper level return idx @@ -247,7 +264,7 @@ class GlTF2Exporter: # traverse into any other property if type(node) in self.__propertyTypeLookup: - return traverse_all_members(node) + return __traverse_property(node) # binary data needs to be moved to a buffer and referenced with a buffer view if isinstance(node, gltf2_io_binary_data.BinaryData): @@ -258,6 +275,21 @@ class GlTF2Exporter: if isinstance(node, gltf2_io_image_data.ImageData): return self.__add_image(node) + # extensions + if isinstance(node, gltf2_io_extensions.Extension): + extension = self.__traverse(node.extension) + self.__append_unique_and_get_index(self.__gltf.extensions_used, node.name) + self.__append_unique_and_get_index(self.__gltf.extensions_required, node.name) + + # extensions that lie in the root of the glTF. + # They need to be converted to a reference at place of occurrence + if isinstance(node, gltf2_io_extensions.ChildOfRootExtension): + root_extension_list = self.__get_key_path(self.__gltf.extensions, [node.name] + node.path) + idx = self.__append_unique_and_get_index(root_extension_list, extension) + return idx + + return extension + # do nothing for any type that does not match a glTF schema (primitives) return node diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py b/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py index a9dc86cfe20b5df2c8bf368109ca073e9a7481da..92b63c7d9859b24484d4beaa4370b7436285ca1b 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py @@ -95,3 +95,4 @@ def from_socket(start_socket: bpy.types.NodeSocket, return __search_from_socket(start_socket, shader_node_filter, []) + diff --git a/io_scene_gltf2/io/com/gltf2_io_extensions.py b/io_scene_gltf2/io/com/gltf2_io_extensions.py new file mode 100644 index 0000000000000000000000000000000000000000..2422d20584d0153ad62e631862c90cc00660d2aa --- /dev/null +++ b/io_scene_gltf2/io/com/gltf2_io_extensions.py @@ -0,0 +1,38 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List, Dict, Any + + +class Extension: + """Container for extensions. Allows to specify requiredness""" + def __init__(self, name: str, extension: Dict[str, Any], required: bool = True): + self.name = name + self.extension = extension + self.required = required + + +class ChildOfRootExtension(Extension): + """Container object for extensions that should be appended to the root extensions""" + def __init__(self, path: List[str], name: str, extension: Dict[str, Any], required: bool = True): + """ + Wrap a local extension entity into an object that will later be inserted into a root extension and converted + to a reference. + :param path: The path of the extension object in the root extension. E.g. ['lights'] for + KHR_lights_punctual. Must be a path to a list in the extensions dict. + :param extension: The data that should be placed into the extension list + """ + self.path = path + super().__init__(name, extension, required) + diff --git a/io_scene_gltf2/io/com/gltf2_io_lights_punctual.py b/io_scene_gltf2/io/com/gltf2_io_lights_punctual.py new file mode 100644 index 0000000000000000000000000000000000000000..3dc8704cceae18505be494cff72e6521a4739e51 --- /dev/null +++ b/io_scene_gltf2/io/com/gltf2_io_lights_punctual.py @@ -0,0 +1,76 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from io_scene_gltf2.io.com.gltf2_io import * + + +class LightSpot: + """light/spot""" + def __init__(self, inner_cone_angle, outer_cone_angle): + self.inner_cone_angle = inner_cone_angle + self.outer_cone_angle = outer_cone_angle + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + inner_cone_angle = from_union([from_float, from_none], obj.get("innerConeAngle")) + outer_cone_angle = from_union([from_float, from_none], obj.get("outerConeAngle")) + return LightSpot(inner_cone_angle, outer_cone_angle) + + def to_dict(self): + result = {} + result["innerConeAngle"] = from_union([from_float, from_none], self.inner_cone_angle) + result["outerConeAngle"] = from_union([from_float, from_none], self.outer_cone_angle) + return result + + +class Light: + """defines a set of lights for use with glTF 2.0. Lights define light sources within a scene""" + def __init__(self, color, intensity, spot, type, range, name, extensions, extras): + self.color = color + self.intensity = intensity + self.spot = spot + self.type = type + self.range = range + self.name = name + self.extensions = extensions + self.extras = extras + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + color = from_union([lambda x: from_list(from_float, x), from_none], obj.get("color")) + intensity = from_union([from_float, from_none], obj.get("intensity")) + spot = LightSpot.from_dict(obj.get("spot")) + type = from_str(obj.get("type")) + range = from_union([from_float, from_none], obj.get("range")) + name = from_union([from_str, from_none], obj.get("name")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + return Light(color, intensity, spot, type, range, name, extensions, extras) + + def to_dict(self): + result = {} + result["color"] = from_union([lambda x: from_list(to_float, x), from_none], self.color) + result["intensity"] = from_union([from_float, from_none], self.intensity) + result["spot"] = from_union([lambda x: to_class(LightSpot, x), from_none], self.spot) + result["type"] = from_str(self.type) + result["range"] = from_union([from_float, from_none], self.range) + result["name"] = from_union([from_str, from_none], self.name) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + return result +