1#!/usr/bin/python
2
3#
4# This source file is part of appleseed.
5# Visit https://appleseedhq.net/ for additional information and resources.
6#
7# This software is released under the MIT license.
8#
9# Copyright (c) 2016-2018 Francois Beaune, The appleseedhq Organization
10#
11# Permission is hereby granted, free of charge, to any person obtaining a copy
12# of this software and associated documentation files (the "Software"), to deal
13# in the Software without restriction, including without limitation the rights
14# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15# copies of the Software, and to permit persons to whom the Software is
16# furnished to do so, subject to the following conditions:
17#
18# The above copyright notice and this permission notice shall be included in
19# all copies or substantial portions of the Software.
20#
21# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27# THE SOFTWARE.
28#
29
30from __future__ import division
31from __future__ import print_function
32from xml.etree.ElementTree import ElementTree
33import appleseed as asr
34import argparse
35import math
36import os.path
37import sys
38import traceback
39
40
41# -------------------------------------------------------------------------------------------------
42# Utility functions.
43# -------------------------------------------------------------------------------------------------
44
45def info(message):
46    print(message)
47
48
49def progress(message):
50    print(message + "...")
51
52
53def warning(message):
54    print("Warning: {0}.".format(message))
55
56
57def fatal(message):
58    print("Fatal: {0}. Aborting.".format(message))
59    # if sys.exc_info()[0]:
60    #     print(traceback.format_exc())
61    sys.exit(1)
62
63
64def square(x):
65    return x * x
66
67
68# -------------------------------------------------------------------------------------------------
69# Conversion code.
70# -------------------------------------------------------------------------------------------------
71
72def get_vector(element):
73    return [
74        float(element.attrib["x"]),
75        float(element.attrib["y"]),
76        float(element.attrib["z"])
77    ]
78
79
80def get_matrix(element):
81    values = [float(x) for x in element.attrib["value"].split()]
82    if len(values) != 16:
83        fatal("Matrix was expected to contain 16 coefficients but contained {0} instead".format(len(values)))
84    matrix = asr.Matrix4d()
85    for i in range(16):
86        matrix[int(i / 4), i % 4] = values[i]
87    return matrix
88
89
90def get_rgb(element):
91    values = [float(x) for x in element.attrib["value"].split(",")]
92    if len(values) != 3:
93        fatal("RGB color was expected to contain 3 coefficients but contained {0} instead".format(len(values)))
94    return values
95
96
97def get_rgb_and_multiplier(element):
98    values = get_rgb(element)
99    max_value = max(values)
100    if max_value > 1.0:
101        return [x / max_value for x in values], max_value
102    else:
103        return values, 1.0
104
105
106def set_private_param(params, key, value):
107    params.setdefault("mitsuba2appleseed", {})[key] = value
108
109
110def get_private_param(params, key, default):
111    return params.setdefault("mitsuba2appleseed", {}).get(key, default)
112
113
114def clear_private_params(params):
115    params.pop("mitsuba2appleseed", None)
116
117
118def make_unique_name(prefix, entities):
119    names = set(entity.get_name() for entity in entities)
120
121    if prefix not in names:
122        return prefix
123
124    n = 1
125    while True:
126        candidate = "{0}{1}".format(prefix, n)
127        if candidate not in names:
128            return candidate
129
130
131def convert_path_integrator(project, element):
132    interactive_config = project.configurations().get_by_name("interactive")
133    final_config = project.configurations().get_by_name("final")
134
135    interactive_config.insert_path("lighting_engine", "pt")
136    final_config.insert_path("lighting_engine", "pt")
137
138    max_depth_element = element.find("integer[@name='maxDepth']")
139    if max_depth_element is not None:
140        max_depth = int(max_depth_element.attrib["value"])
141        interactive_config.insert_path("pt.max_path_length", max_depth)
142        final_config.insert_path("pt.max_path_length", max_depth)
143
144
145def convert_sppm_integrator(project, element):
146    interactive_config = project.configurations().get_by_name("interactive")
147    final_config = project.configurations().get_by_name("final")
148
149    interactive_config.insert_path("lighting_engine", "pt")     # SPPM is incompatible with interactive rendering
150    final_config.insert_path("lighting_engine", "sppm")
151
152    final_config.insert_path("sppm.photon_type", "poly")
153
154    max_depth_element = element.find("integer[@name='maxDepth']")
155    if max_depth_element is not None:
156        max_depth = int(max_depth_element.attrib["value"])
157        interactive_config.insert_path("pt.max_path_length", max_depth)
158        final_config.insert_path("sppm.photon_tracing_max_path_length", max_depth)
159        final_config.insert_path("sppm.path_tracing_max_path_length", max_depth)
160
161    photon_count_element = element.find("integer[@name='photonCount']")
162    if photon_count_element is not None:
163        photon_count = int(photon_count_element.attrib["value"])
164        final_config.insert_path("sppm.light_photons_per_pass", photon_count)
165        final_config.insert_path("sppm.env_photons_per_pass", photon_count)
166
167    initial_radius_element = element.find("float[@name='initialRadius']")
168    if initial_radius_element is not None:
169        initial_radius = float(initial_radius_element.attrib["value"])
170        # todo: this is mostly incorrect as in appleseed the initial radius is expressed
171        # as a percentage of the scene's radius, while in Mitsuba it is a world space
172        # distance.
173        final_config.insert_path("sppm.initial_radius", initial_radius)
174
175    alpha_element = element.find("float[@name='alpha']")
176    if alpha_element is not None:
177        alpha = float(alpha_element.attrib["value"])
178        final_config.insert_path("sppm.alpha", alpha)
179
180    pass_count_element = element.find("integer[@name='maxPasses']")
181    if pass_count_element is not None:
182        pass_count = int(pass_count_element.attrib["value"])
183        project.configurations().get_by_name("final").insert_path("generic_frame_renderer.passes", pass_count)
184
185
186def convert_integrator(project, element):
187    type = element.attrib["type"]
188    if type == "path":
189        convert_path_integrator(project, element)
190    elif type == "sppm":
191        convert_sppm_integrator(project, element)
192    else:
193        warning("Don't know how to convert integrator of type {0}, defaulting to path integrator.".format(type))
194        convert_path_integrator(project, element)
195
196
197def convert_film(camera_params, frame_params, element):
198    width = int(element.find("integer[@name='width']").attrib["value"])
199    height = int(element.find("integer[@name='height']").attrib["value"])
200    dimensions = "{0} {1}".format(width, height)
201    camera_params["film_dimensions"] = dimensions
202    frame_params["resolution"] = dimensions
203
204
205def convert_sampler(project, element):
206    sample_count_element = element.find("integer[@name='sampleCount']")
207    if sample_count_element is not None:
208        sample_count = int(sample_count_element.attrib["value"])
209        project.configurations().get_by_name("final").insert_path("uniform_pixel_renderer.samples", sample_count)
210
211
212def convert_sensor(project, scene, element):
213    camera_params = {}
214    camera_matrix = None
215    frame_params = {
216        "camera": "camera",
217        "color_space": "srgb",
218        "tile_size": "32 32"
219    }
220
221    for child in element:
222        if child.tag == "float":
223            if child.attrib["name"] == "fov":
224                camera_params["horizontal_fov"] = child.attrib["value"]
225        elif child.tag == "transform":
226            camera_matrix = get_matrix(child.find("matrix"))
227        elif child.tag == "sampler":
228            convert_sampler(project, child)
229        elif child.tag == "film":
230            convert_film(camera_params, frame_params, child)
231
232    camera = asr.Camera("pinhole_camera", "camera", camera_params)
233    if camera_matrix is not None:
234        roty = asr.Matrix4d.make_rotation(asr.Vector3d(0.0, 1.0, 0.0), math.radians(180.0))
235        camera_matrix = camera_matrix * roty
236        camera.transform_sequence().set_transform(0.0, asr.Transformd(camera_matrix))
237    scene.cameras().insert(camera)
238
239    project.set_frame(asr.Frame("beauty", frame_params))
240
241
242def create_linear_rgb_color(parent, color_name, rgb, multiplier):
243    color_params = {
244        "color_space": "linear_rgb",
245        "multiplier": multiplier
246    }
247    parent.colors().insert(asr.ColorEntity(color_name, color_params, rgb))
248
249
250def is_hdri_file(filepath):
251    return filepath.endswith(".exr") or filepath.endswith(".hdr") or filepath.endswith(".pfm")
252
253
254def create_texture(parent, texture_name, filepath):
255    parent.textures().insert(asr.Texture("disk_texture_2d", texture_name, {
256        "filename": filepath,
257        "color_space": "linear_rgb" if is_hdri_file(filepath) else "srgb"
258    }, []))
259
260    texture_instance_name = "{0}_inst".format(texture_name)
261    texture_instance = asr.TextureInstance(texture_instance_name, {}, texture_name, asr.Transformf.identity())
262    parent.texture_instances().insert(texture_instance)
263    return texture_instance_name
264
265
266def convert_texture(parent, texture_name, element):
267    type = element.attrib["type"]
268    if type == "bitmap":
269        filepath = element.find("string[@name='filename']").attrib["value"]
270        return create_texture(parent, texture_name, filepath)
271    else:
272        warning("Don't know how to convert texture of type {0}".format(type))
273        color_params = {
274            "color_space": "srgb",
275            "multiplier": 1.0
276        }
277        parent.colors().insert(asr.ColorEntity(texture_name, color_params, [0.7, 0.7, 0.7]))
278        return texture_name
279
280
281def convert_colormap(parent, parent_name, element):
282    map_name = element.attrib["name"]
283    if element.tag == "texture":
284        texture_name = "{0}_{1}".format(parent_name, map_name)
285        return convert_texture(parent, texture_name, element)
286    elif element.tag == "rgb":
287        color_name = "{0}_{1}".format(parent_name, map_name)
288        rgb, multiplier = get_rgb_and_multiplier(element)
289        create_linear_rgb_color(parent, color_name, rgb, multiplier)
290        return color_name
291    else:
292        warning("Don't know how to convert color map of type {0}".format(element.tag))
293
294
295def convert_alpha_to_roughness(element, default_alpha):
296    alpha_element = element.find("float[@name='alpha']")
297    alpha = float(alpha_element.attrib["value"]) if alpha_element is not None else default_alpha
298    return math.sqrt(alpha)
299
300
301def fresnel_conductor_inverse_reparam(n, k):
302    # See artist_friendly_fresnel_conductor_inverse_reparameterization() function
303    # in src/appleseed/foundation/math/fresnel.h.
304
305    normal_reflectance = []
306    edge_tint = []
307
308    for ni, ki in zip(n, k):
309        k2 = square(ki)
310        r = (square(ni - 1.0) + k2) / (square(ni + 1.0) + k2)
311        normal_reflectance.append(r)
312
313        sqrt_r = math.sqrt(r)
314        tmp = (1.0 + sqrt_r) / (1.0 - sqrt_r)
315        edge_tint.append(max((tmp - ni) / (tmp - ((1.0 - r) / (1.0 + r))), 0))
316
317    return normal_reflectance, edge_tint
318
319
320def convert_diffuse_bsdf(assembly, bsdf_name, element):
321    bsdf_params = {}
322
323    reflectance = element.find("*[@name='reflectance']")
324    bsdf_params["reflectance"] = convert_colormap(assembly, bsdf_name, reflectance)
325
326    assembly.bsdfs().insert(asr.BSDF("lambertian_brdf", bsdf_name, bsdf_params))
327
328
329def convert_roughdiffuse_bsdf(assembly, bsdf_name, element):
330    bsdf_params = {}
331
332    reflectance = element.find("*[@name='reflectance']")
333    bsdf_params["reflectance"] = convert_colormap(assembly, bsdf_name, reflectance)
334
335    bsdf_params["roughness"] = convert_alpha_to_roughness(element, 0.2)
336
337    assembly.bsdfs().insert(asr.BSDF("orennayar_brdf", bsdf_name, bsdf_params))
338
339
340def convert_plastic_bsdf(assembly, bsdf_name, element, roughness=0.0):
341    bsdf_params = {}
342
343    distribution_element = element.find("string[@name='distribution']")
344    if distribution_element is not None:
345        distribution = distribution_element.attrib["value"]
346        if distribution == "phong":
347            warning("Phong distribution not supported by appleseed's plastic BRDF, defaulting to GGX")
348            distribution = "ggx"
349        bsdf_params["mdf"] = distribution
350    else:
351        bsdf_params["mdf"] = "beckmann"
352
353    specular_reflectance_element = element.find("*[@name='specularReflectance']")
354    bsdf_params["specular_reflectance"] = convert_colormap(assembly, bsdf_name, specular_reflectance_element) \
355        if specular_reflectance_element is not None else 1.0
356
357    bsdf_params["roughness"] = roughness
358
359    diffuse_reflectance_element = element.find("*[@name='diffuseReflectance']")
360    bsdf_params["diffuse_reflectance"] = convert_colormap(assembly, bsdf_name, diffuse_reflectance_element) \
361        if diffuse_reflectance_element is not None else 0.5
362
363    ior_element = element.find("float[@name='intIOR']")
364    bsdf_params["ior"] = float(ior_element.attrib["value"]) if ior_element is not None else 1.49
365
366    nonlinear_element = element.find("boolean[@name='nonlinear']")
367    bsdf_params["internal_scattering"] = 1.0 if nonlinear_element is not None and \
368        nonlinear_element.attrib["value"] == "true" else 0.0
369
370    assembly.bsdfs().insert(asr.BSDF("plastic_brdf", bsdf_name, bsdf_params))
371
372
373def convert_roughplastic_bsdf(assembly, bsdf_name, element):
374    roughness = convert_alpha_to_roughness(element, 0.1)
375    return convert_plastic_bsdf(assembly, bsdf_name, element, roughness)
376
377
378def convert_conductor_bsdf(assembly, bsdf_name, element, roughness=0.0):
379    bsdf_params = {}
380
381    material_element = element.find("string[@name='material']")
382    if material_element is not None:
383        material = material_element.attrib["value"]
384        if material == "none":
385            bsdf_params["mdf"] = "ggx"
386            bsdf_params["normal_reflectance"] = 1.0
387            bsdf_params["edge_tint"] = 0.0
388            bsdf_params["roughness"] = roughness
389            assembly.bsdfs().insert(asr.BSDF("metal_brdf", bsdf_name, bsdf_params))
390            return
391
392    eta_element = element.find("rgb[@name='eta']")
393    eta_rgb = get_rgb(eta_element)
394
395    k_element = element.find("rgb[@name='k']")
396    k_rgb = get_rgb(k_element)
397
398    normal_reflectance_rgb, edge_tint_rgb = fresnel_conductor_inverse_reparam(eta_rgb, k_rgb)
399
400    normal_reflectance_color_name = "{0}_normal_reflectance".format(bsdf_name)
401    create_linear_rgb_color(assembly, normal_reflectance_color_name, normal_reflectance_rgb, 1.0)
402
403    edge_tint_color_name = "{0}_edge_tint".format(bsdf_name)
404    create_linear_rgb_color(assembly, edge_tint_color_name, edge_tint_rgb, 1.0)
405
406    bsdf_params["mdf"] = "ggx"
407    bsdf_params["normal_reflectance"] = normal_reflectance_color_name
408    bsdf_params["edge_tint"] = edge_tint_color_name
409    bsdf_params["roughness"] = roughness
410
411    specular_reflectance_element = element.find("*[@name='specularReflectance']")
412    bsdf_params["reflectance_multiplier"] = convert_colormap(assembly, bsdf_name, specular_reflectance_element) \
413        if specular_reflectance_element is not None else 1.0
414
415    assembly.bsdfs().insert(asr.BSDF("metal_brdf", bsdf_name, bsdf_params))
416
417
418def convert_roughconductor_bsdf(assembly, bsdf_name, element):
419    roughness = convert_alpha_to_roughness(element, 0.1)
420    return convert_conductor_bsdf(assembly, bsdf_name, element, roughness)
421
422
423def convert_dielectric_bsdf(assembly, bsdf_name, element, roughness=0.0):
424    bsdf_params = {}
425
426    # todo: support textured IOR.
427    bsdf_params["ior"] = float(element.find("float[@name='intIOR']").attrib["value"])
428
429    bsdf_params["mdf"] = "ggx"
430    bsdf_params["surface_transmittance"] = 1.0
431    bsdf_params["roughness"] = roughness
432
433    specular_reflectance_element = element.find("*[@name='specularReflectance']")
434    bsdf_params["reflection_tint"] = convert_colormap(assembly, bsdf_name, specular_reflectance_element) \
435        if specular_reflectance_element is not None else 1.0
436
437    specular_transmittance_element = element.find("*[@name='specularTransmittance']")
438    bsdf_params["refraction_tint"] = convert_colormap(assembly, bsdf_name, specular_transmittance_element) \
439        if specular_transmittance_element is not None else 1.0
440
441    assembly.bsdfs().insert(asr.BSDF("glass_bsdf", bsdf_name, bsdf_params))
442
443
444def convert_roughdielectric_bsdf(assembly, bsdf_name, element):
445    roughness = convert_alpha_to_roughness(element, 0.1)
446    return convert_dielectric_bsdf(assembly, bsdf_name, element, roughness)
447
448
449def convert_area_emitter(assembly, emitter_name, element):
450    if emitter_name is None:
451        fatal("Area emitters must have a name")
452
453    edf_params = {}
454
455    radiance = element.find("*[@name='radiance']")
456    edf_params["radiance"] = convert_colormap(assembly, emitter_name, radiance)
457
458    assembly.edfs().insert(asr.EDF("diffuse_edf", emitter_name, edf_params))
459
460
461def convert_constant_emitter(scene, emitter_name, element):
462    radiance = element.find("*[@name='radiance']")
463
464    scene.environment_edfs().insert(asr.EnvironmentEDF("constant_environment_edf", "environment_edf", {
465        "radiance": convert_colormap(scene, emitter_name, radiance)
466    }))
467
468    scene.environment_shaders().insert(asr.EnvironmentShader("edf_environment_shader", "environment_shader", {
469        "environment_edf": 'environment_edf'
470    }))
471
472    scene.set_environment(asr.Environment("environment", {
473        "environment_edf": "environment_edf",
474        "environment_shader": "environment_shader"
475    }))
476
477
478def convert_envmap_emitter(scene, emitter_name, element):
479    filepath = element.find("string[@name='filename']").attrib["value"]
480
481    texture_instance_name = create_texture(scene, "environment_map", filepath)
482
483    env_edf = asr.EnvironmentEDF("latlong_map_environment_edf", "environment_edf", {
484        "radiance": texture_instance_name
485    })
486
487    matrix_element = element.find("transform[@name='toWorld']/matrix")
488    if matrix_element is not None:
489        matrix = get_matrix(matrix_element)
490        roty = asr.Matrix4d.make_rotation(asr.Vector3d(0.0, 1.0, 0.0), math.radians(-90.0))
491        matrix = matrix * roty
492        env_edf.transform_sequence().set_transform(0.0, asr.Transformd(matrix))
493
494    scene.environment_edfs().insert(env_edf)
495
496    scene.environment_shaders().insert(asr.EnvironmentShader("edf_environment_shader", "environment_shader", {
497        "environment_edf": 'environment_edf'
498    }))
499
500    scene.set_environment(asr.Environment("environment", {
501        "environment_edf": "environment_edf",
502        "environment_shader": "environment_shader"
503    }))
504
505
506def convert_sun_emitter(scene, assembly, emitter_name, element):
507    sun_params = {}
508
509    turbidity = element.find("float[@name='turbidity']")
510    if turbidity is not None:
511        sun_params["turbidity"] = float(turbidity.attrib["value"]) - 2.0
512    else:
513        sun_params["turbidity"] = 1.0
514
515    scale = element.find("float[@name='scale']")
516    if scale is not None:
517        sun_params["radiance_multiplier"] = float(scale.attrib["value"])
518
519    sun = asr.Light("sun_light", "sun_light", sun_params)
520
521    sun_direction = element.find("vector[@name='sunDirection']")
522    if sun_direction is not None:
523        from_direction = asr.Vector3d(0.0, 0.0, 1.0)
524        to_direction = asr.Vector3d(get_vector(sun_direction))
525        sun.set_transform(
526            asr.Transformd(
527                asr.Matrix4d.make_rotation(
528                    asr.Quaterniond.make_rotation(from_direction, to_direction))))
529
530    assembly.lights().insert(sun)
531
532
533def convert_sunsky_emitter(scene, assembly, emitter_name, element):
534    turbidity_element = element.find("float[@name='turbidity']")
535    if turbidity_element is not None:
536        turbidity = float(turbidity_element.attrib["value"]) - 2.0
537    else:
538        turbidity = 1.0
539
540    # Sky.
541    sun_direction = element.find("vector[@name='sunDirection']")
542    if sun_direction is not None:
543        d = get_vector(sun_direction)
544        sun_theta = math.acos(d[1])
545        sun_phi = math.atan2(d[2], d[0])
546    else:
547        sun_theta = 0.0
548        sun_phi = 0.0
549    env_edf = asr.EnvironmentEDF("hosek_environment_edf", "environment_edf", {
550        "sun_theta": math.degrees(sun_theta),
551        "sun_phi": math.degrees(sun_phi),
552        "turbidity": turbidity
553    })
554    scene.environment_edfs().insert(env_edf)
555    scene.environment_shaders().insert(asr.EnvironmentShader("edf_environment_shader", "environment_shader", {
556        "environment_edf": 'environment_edf'
557    }))
558    scene.set_environment(asr.Environment("environment", {
559        "environment_edf": "environment_edf",
560        "environment_shader": "environment_shader"
561    }))
562
563    # Sun.
564    sun_params = {"environment_edf": "environment_edf", "turbidity": turbidity}
565    sun_scale = element.find("float[@name='sunScale']")
566    if sun_scale is not None:
567        sun_params["radiance_multiplier"] = float(sun_scale.attrib["value"])
568    sun = asr.Light("sun_light", "sun_light", sun_params)
569    assembly.lights().insert(sun)
570
571
572def convert_emitter(scene, assembly, emitter_name, element):
573    type = element.attrib["type"]
574    if type == "area":
575        convert_area_emitter(assembly, emitter_name, element)
576    elif type == "constant":
577        convert_constant_emitter(scene, emitter_name, element)
578    elif type == "envmap":
579        convert_envmap_emitter(scene, emitter_name, element)
580    elif type == "sun":
581        convert_sun_emitter(scene, assembly, emitter_name, element)
582    elif type == "sunsky":
583        convert_sunsky_emitter(scene, assembly, emitter_name, element)
584    else:
585        warning("Don't know how to convert emitter of type {0}".format(type))
586
587
588def convert_material(assembly, material_name, material_params, element):
589    if material_name is None and "id" in element.attrib:
590        material_name = element.attrib["id"]
591
592    type = element.attrib["type"]
593
594    # Two-sided adapter.
595    if type == "twosided":
596        set_private_param(material_params, "two_sided", True)
597        return convert_material(assembly, material_name, material_params, element.find("bsdf"))
598
599    # Bump mapping adapter.
600    # todo: add bump mapping support.
601    if type == "bumpmap":
602        return convert_material(assembly, material_name, material_params, element.find("bsdf"))
603
604    # Opacity adapter.
605    if type == "mask":
606        opacity_element = element.find("*[@name='opacity']")
607        opacity = 0.5
608        if opacity_element is not None:
609            if opacity_element.tag == "rgb":
610                opacity_rgb = get_rgb(opacity_element)
611                if opacity_rgb[0] == opacity_rgb[1] and opacity_rgb[0] == opacity_rgb[2]:
612                    opacity = opacity_rgb[0]
613                else:
614                    warning("Colored opacity not supported, using average opacity")
615                    opacity = (opacity_rgb[0] + opacity_rgb[1] + opacity_rgb[2]) / 3
616            else:
617                warning("Textured opacity not supported")
618        material_params["alpha_map"] = opacity
619        return convert_material(assembly, material_name, material_params, element.find("bsdf"))
620
621    # BSDF.
622    bsdf_name = "{0}_bsdf".format(material_name)
623    if type == "diffuse":
624        convert_diffuse_bsdf(assembly, bsdf_name, element)
625    elif type == "roughdiffuse":
626        convert_roughdiffuse_bsdf(assembly, bsdf_name, element)
627    elif type == "plastic":
628        convert_plastic_bsdf(assembly, bsdf_name, element)
629    elif type == "roughplastic":
630        convert_roughplastic_bsdf(assembly, bsdf_name, element)
631    elif type == "conductor":
632        convert_conductor_bsdf(assembly, bsdf_name, element)
633    elif type == "roughconductor":
634        convert_roughconductor_bsdf(assembly, bsdf_name, element)
635    elif type == "dielectric":
636        set_private_param(material_params, "two_sided", True)
637        convert_dielectric_bsdf(assembly, bsdf_name, element)
638    elif type == "roughdielectric":
639        set_private_param(material_params, "two_sided", True)
640        convert_roughdielectric_bsdf(assembly, bsdf_name, element)
641    elif type == "thindielectric":
642        set_private_param(material_params, "two_sided", True)
643        set_private_param(material_params, "thin_dielectric", True)
644        convert_dielectric_bsdf(assembly, bsdf_name, element)
645    else:
646        warning("Don't know how to convert BSDF of type {0}".format(type))
647        return
648
649    # Hack: force light-emitting materials to be single-sided.
650    if "edf" in material_params:
651        set_private_param(material_params, "two_sided", False)
652
653    # Material.
654    material_params["bsdf"] = bsdf_name
655    material_params["surface_shader"] = "physical_surface_shader"
656    assembly.materials().insert(asr.Material("generic_material", material_name, material_params))
657
658
659def process_shape_material(scene, assembly, instance_name, element):
660    material = None
661
662    # Material reference.
663    ref_element = element.find("ref")
664    if ref_element is not None:
665        material_name = ref_element.attrib["id"]
666        material = assembly.materials().get_by_name(material_name)
667
668    # Embedded material (has priority over the referenced material).
669    bsdf_element = element.find("bsdf")
670    if bsdf_element is not None:
671        material_name = "{0}_material".format(instance_name)
672        convert_material(assembly, material_name, {}, bsdf_element)
673        material = assembly.materials().get_by_name(material_name)
674
675    # Embedded emitter (we suppose it's an area emitter).
676    emitter_element = element.find("emitter")
677    if emitter_element is not None:
678        edf_name = "{0}_edf".format(instance_name)
679        convert_emitter(scene, assembly, edf_name, emitter_element)
680
681        material_params = material.get_parameters()
682        material_params["edf"] = edf_name
683
684        # Hack: force light-emitting materials to be single-sided.
685        set_private_param(material_params, "two_sided", False)
686
687        material_name = make_unique_name(instance_name + "_material", assembly.materials())
688        material = asr.Material("generic_material", material_name, material_params)
689        assembly.materials().insert(material)
690        material = assembly.materials().get_by_name(material_name)
691
692    return material.get_name() if material is not None else None
693
694
695def make_object_instance(assembly, object, material_name, transform):
696    instance_name = "{0}_inst".format(object.get_name())
697
698    front_mappings = {}
699    back_mappings = {}
700    instance_params = {}
701
702    if material_name is not None:
703        material = assembly.materials().get_by_name(material_name)
704        two_sided = get_private_param(material.get_parameters(), "two_sided", False) if material is not None else False
705        thin_dielectric = get_private_param(material.get_parameters(), "thin_dielectric", False) if material is not None else False
706
707        slots = object.material_slots()
708        if len(slots) == 0:
709            slots = ["default"]
710
711        front_mappings = dict([(slot, material_name) for slot in slots])
712        back_mappings = front_mappings if two_sided else {}
713
714        if thin_dielectric:
715            instance_params = {"visibility": {"shadow": False}}
716
717    return asr.ObjectInstance(instance_name, instance_params, object.get_name(), transform, front_mappings, back_mappings)
718
719
720def make_new_object_name(assembly):
721    return "object_{0}".format(len(assembly.objects()))
722
723
724def convert_obj_shape(project, scene, assembly, element):
725    # Read OBJ file from disk and create objects.
726    object_name = make_new_object_name(assembly)
727    filepath = element.find("string[@name='filename']").attrib["value"]
728    objects = asr.MeshObjectReader.read(project.get_search_paths(), object_name, {"filename": filepath})
729
730    # Instance transform.
731    matrix = get_matrix(element.find("transform[@name='toWorld']/matrix"))
732    transform = asr.Transformd(matrix)
733
734    # Instance material.
735    material_name = process_shape_material(scene, assembly, object_name, element)
736
737    for object in objects:
738        instance = make_object_instance(assembly, object, material_name, transform)
739        assembly.object_instances().insert(instance)
740        assembly.objects().insert(object)
741
742
743def convert_rectangle_shape(scene, assembly, element):
744    # Object.
745    object_name = make_new_object_name(assembly)
746    object = asr.create_primitive_mesh(object_name, {
747        "primitive": "grid",
748        "resolution_u": 1,
749        "resolution_v": 1,
750        "width": 2.0,
751        "height": 2.0
752    })
753
754    # Instance transform.
755    matrix = get_matrix(element.find("transform[@name='toWorld']/matrix"))
756    rotx = asr.Matrix4d.make_rotation(asr.Vector3d(1.0, 0.0, 0.0), math.radians(90.0))
757    matrix = matrix * rotx
758    transform = asr.Transformd(matrix)
759
760    # Instance material.
761    material_name = process_shape_material(scene, assembly, object_name, element)
762
763    instance = make_object_instance(assembly, object, material_name, transform)
764    assembly.object_instances().insert(instance)
765    assembly.objects().insert(object)
766
767
768def convert_disk_shape(scene, assembly, element):
769    # Radius.
770    radius_element = element.find("float[@name='radius']")
771    radius = float(radius_element.attrib["value"]) if radius_element is not None else 1.0
772
773    # Object.
774    object_name = make_new_object_name(assembly)
775    object = asr.create_primitive_mesh(object_name, {
776        "primitive": "disk",
777        "resolution_u": 1,
778        "resolution_v": 32,
779        "radius": radius
780    })
781
782    # Instance transform.
783    matrix = get_matrix(element.find("transform[@name='toWorld']/matrix"))
784    rotx = asr.Matrix4d.make_rotation(asr.Vector3d(1.0, 0.0, 0.0), math.radians(90.0))
785    matrix = matrix * rotx
786    transform = asr.Transformd(matrix)
787
788    # Instance material.
789    material_name = process_shape_material(scene, assembly, object_name, element)
790
791    instance = make_object_instance(assembly, object, material_name, transform)
792    assembly.object_instances().insert(instance)
793    assembly.objects().insert(object)
794
795
796def convert_sphere_shape(scene, assembly, element):
797    # Radius.
798    radius_element = element.find("float[@name='radius']")
799    radius = float(radius_element.attrib["value"]) if radius_element is not None else 1.0
800
801    # Center.
802    center_element = element.find("point[@name='center']")
803    center = asr.Vector3d(get_vector(center_element)) if center_element is not None else asr.Vector3d(0.0)
804
805    # Object.
806    object_name = make_new_object_name(assembly)
807    object = asr.create_primitive_mesh(object_name, {
808        "primitive": "sphere",
809        "resolution_u": 32,
810        "resolution_v": 16,
811        "radius": radius
812    })
813
814    # Instance transform.
815    matrix = asr.Matrix4d.make_translation(center)
816    matrix_element = element.find("transform[@name='toWorld']/matrix")
817    if matrix_element is not None:
818        # todo: no idea what is the right multiplication order, untested.
819        matrix = matrix * get_matrix(matrix_element)
820    transform = asr.Transformd(matrix)
821
822    # Instance material.
823    material_name = process_shape_material(scene, assembly, object_name, element)
824
825    instance = make_object_instance(assembly, object, material_name, transform)
826    assembly.object_instances().insert(instance)
827    assembly.objects().insert(object)
828
829
830def convert_cube_shape(scene, assembly, element):
831    # Object.
832    object_name = make_new_object_name(assembly)
833    object = asr.create_primitive_mesh(object_name, {"primitive": "cube"})
834
835    # Instance transform.
836    matrix_element = element.find("transform[@name='toWorld']/matrix")
837    matrix = get_matrix(matrix_element) if matrix_element is not None else asr.Matrix4d.identity()
838    transform = asr.Transformd(matrix)
839
840    # Instance material.
841    material_name = process_shape_material(scene, assembly, object_name, element)
842
843    instance = make_object_instance(assembly, object, material_name, transform)
844    assembly.object_instances().insert(instance)
845    assembly.objects().insert(object)
846
847
848def convert_shape(project, scene, assembly, element):
849    type = element.attrib["type"]
850    if type == "obj":
851        convert_obj_shape(project, scene, assembly, element)
852    elif type == "rectangle":
853        convert_rectangle_shape(scene, assembly, element)
854    elif type == "disk":
855        convert_disk_shape(scene, assembly, element)
856    elif type == "sphere":
857        convert_sphere_shape(scene, assembly, element)
858    elif type == "cube":
859        convert_cube_shape(scene, assembly, element)
860    else:
861        warning("Don't know how to convert shape of type {0}".format(type))
862
863
864def convert_scene(project, scene, assembly, element):
865    for child in element:
866        if child.tag == "integrator":
867            convert_integrator(project, child)
868        elif child.tag == "sensor":
869            convert_sensor(project, scene, child)
870        elif child.tag == "bsdf":
871            convert_material(assembly, None, {}, child)
872        elif child.tag == "shape":
873            convert_shape(project, scene, assembly, child)
874        elif child.tag == "emitter":
875            convert_emitter(scene, assembly, None, child)
876
877
878def convert(tree):
879    project = asr.Project("project")
880
881    # Search paths.
882    paths = project.get_search_paths()
883    paths.append("models")
884    paths.append("textures")
885    project.set_search_paths(paths)
886
887    # Add default configurations to the project.
888    project.add_default_configurations()
889
890    # Enable caustics.
891    project.configurations().get_by_name("final").insert_path("pt.enable_caustics", True)
892    project.configurations().get_by_name("interactive").insert_path("pt.enable_caustics", True)
893
894    # Create a scene.
895    scene = asr.Scene()
896
897    # Create an assembly.
898    assembly = asr.Assembly("assembly")
899    assembly.surface_shaders().insert(asr.SurfaceShader("physical_surface_shader", "physical_surface_shader"))
900
901    # Convert the Mitsuba scene.
902    convert_scene(project, scene, assembly, tree.getroot())
903
904    # Create an instance of the assembly.
905    assembly_inst = asr.AssemblyInstance("assembly_inst", {}, assembly.get_name())
906    assembly_inst.transform_sequence().set_transform(0.0, asr.Transformd(asr.Matrix4d.identity()))
907    scene.assembly_instances().insert(assembly_inst)
908
909    # Insert the assembly into the scene.
910    scene.assemblies().insert(assembly)
911
912    # Bind the scene to the project.
913    project.set_scene(scene)
914
915    return project
916
917
918# -------------------------------------------------------------------------------------------------
919# Entry point.
920# -------------------------------------------------------------------------------------------------
921
922def main():
923    parser = argparse.ArgumentParser(description="convert Mitsuba scenes to appleseed format.")
924    parser.add_argument("input_file", metavar="input-file", help="Mitsuba scene (*.xml)")
925    parser.add_argument("output_file", metavar="output-file", help="appleseed scene (*.appleseed)")
926    args = parser.parse_args()
927
928    # Create a log target that outputs to stderr, and binds it to the renderer's global logger.
929    # Eventually you will want to redirect log messages to your own target.
930    # For this you will need to subclass appleseed.ILogTarget.
931    log_target = asr.ConsoleLogTarget(sys.stderr)
932
933    # It is important to keep log_target alive, as the global logger does not
934    # take ownership of it. In this example, we do that by removing the log target
935    # when no longer needed, at the end of this function.
936    asr.global_logger().add_target(log_target)
937    asr.global_logger().set_verbosity_level(asr.LogMessageCategory.Warning)
938
939    tree = ElementTree()
940    try:
941        tree.parse(args.input_file)
942    except IOError:
943        fatal("Failed to load {0}".format(args.input_file))
944
945    # Make asset paths in the Mitsuba file relative to the Mitsuba file itself.
946    for child in tree.getroot():
947        filepath = child.find("string[@name='filename']")
948        if filepath is not None:
949            filepath.attrib["value"] = os.path.join(os.path.dirname(args.input_file), filepath.attrib["value"])
950
951    project = convert(tree)
952
953    asr.ProjectFileWriter().write(project, args.output_file,
954                                  asr.ProjectFileWriterOptions.OmitHandlingAssetFiles)
955
956if __name__ == '__main__':
957    main()
958