1 # SPDX-License-Identifier: GPL-2.0-or-later
3 """With respect to camera frame and optics distortions, also export environment
5 with world, sky, atmospheric effects such as rainbows or smoke """
10 from imghdr
import what
# imghdr is a python lib to identify image file types
11 from math
import atan
, pi
, sqrt
, degrees
12 from . import voxel_lib
# for smoke rendering
13 from .model_primitives
import write_object_modifiers
16 # -------- find image texture # used for export_world -------- #
19 def image_format(img_f
):
20 """Identify input image filetypes to transmit to POV."""
21 # First use the below explicit extensions to identify image file prospects
35 }.get(os
.path
.splitext(img_f
)[-1].upper(), "")
36 # Then, use imghdr to really identify the filetype as it can be different
38 # maybe add a check for if path exists here?
39 print(" WARNING: texture image has no extension") # too verbose
41 ext
= what(img_f
) # imghdr is a python lib to identify image file types
46 """Translate mapping type from Blender UI to POV syntax and return that string."""
48 texdata
= bpy
.data
.textures
[ts
.texture
]
49 if ts
.mapping
== "FLAT":
50 image_map
= "map_type 0 "
51 elif ts
.mapping
== "SPHERE":
52 image_map
= "map_type 1 "
53 elif ts
.mapping
== "TUBE":
54 image_map
= "map_type 2 "
56 # map_type 3 and 4 in development (?) (ENV in pov 3.8)
57 # for POV-Ray, currently they just seem to default back to Flat (type 0)
58 # elif ts.mapping=="?":
59 # image_map = " map_type 3 "
60 # elif ts.mapping=="?":
61 # image_map = " map_type 4 "
62 if ts
.use_interpolation
: # Available if image sampling class reactivated?
63 image_map
+= " interpolate 2 "
64 if texdata
.extension
== "CLIP":
67 # if ts.mapping=='CUBE':
68 # image_map+= "warp { cubic } rotate <-90,0,180>"
69 # no direct cube type mapping. Though this should work in POV 3.7
70 # it doesn't give that good results(best suited to environment maps?)
72 # print(" No texture image found ")
76 def img_map_transforms(ts
):
77 """Translate mapping transformations from Blender UI to POV syntax and return that string."""
78 # XXX TODO: unchecked textures give error of variable referenced before assignment XXX
79 # POV-Ray "scale" is not a number of repetitions factor, but ,its
80 # inverse, a standard scale factor.
81 # 0.5 Offset is needed relatively to scale because center of the
82 # scale is 0.5,0.5 in blender and 0,0 in POV
83 # Strange that the translation factor for scale is not the same as for
85 # TODO: verify both matches with other blender renderers / internal in previous versions.
86 image_map_transforms
= ""
87 image_map_transforms
= "scale <%.4g,%.4g,%.4g> translate <%.4g,%.4g,%.4g>" % (
95 # image_map_transforms = (" translate <-0.5,-0.5,0.0> scale <%.4g,%.4g,%.4g> translate <%.4g,%.4g,%.4g>" % \
99 # (0.5 / ts.scale.x) + ts.offset.x,
100 # (0.5 / ts.scale.y) + ts.offset.y,
102 # image_map_transforms = (
103 # "translate <-0.5,-0.5,0> "
104 # "scale <-1,-1,1> * <%.4g,%.4g,%.4g> "
105 # "translate <0.5,0.5,0> + <%.4g,%.4g,%.4g>" % \
113 return image_map_transforms
117 """Translate world mapping from Blender UI to POV syntax and return that string."""
118 tex
= bpy
.data
.textures
[wts
.texture
]
120 # texture_coords refers to the mapping of world textures:
121 if wts
.texture_coords
in ["VIEW", "GLOBAL"]:
122 image_mapBG
= " map_type 0 "
123 elif wts
.texture_coords
== "ANGMAP":
124 image_mapBG
= " map_type 1 "
125 elif wts
.texture_coords
== "TUBE":
126 image_mapBG
= " map_type 2 "
128 if tex
.use_interpolation
:
129 image_mapBG
+= " interpolate 2 "
130 if tex
.extension
== "CLIP":
131 image_mapBG
+= " once "
133 # if wts.mapping == 'CUBE':
134 # image_mapBG += "warp { cubic } rotate <-90,0,180>"
135 # no direct cube type mapping. Though this should work in POV 3.7
136 # it doesn't give that good results(best suited to environment maps?)
137 # if image_mapBG == "":
138 # print(" No background texture image found ")
142 def path_image(image
):
143 """Conform a path string to POV syntax to avoid POV errors."""
144 return bpy
.path
.abspath(image
.filepath
, library
=image
.library
).replace("\\", "/")
145 # .replace("\\","/") to get only forward slashes as it's what POV prefers,
149 # end find image texture
150 # -----------------------------------------------------------------------------
153 def export_camera(file, scene
, global_matrix
, render
, tab_write
):
154 """Translate camera from Blender UI to POV syntax and write to exported file."""
155 camera
= scene
.camera
157 # DH disabled for now, this isn't the correct context
158 active_object
= None # bpy.context.active_object # does not always work MR
159 matrix
= global_matrix
@ camera
.matrix_world
160 focal_point
= camera
.data
.dof
.focus_distance
163 q_size
= render
.resolution_x
/ render
.resolution_y
164 tab_write(file, "#declare camLocation = <%.6f, %.6f, %.6f>;\n" % matrix
.translation
[:])
168 "#declare camLookAt = <%.6f, %.6f, %.6f>;\n"
169 % tuple(degrees(e
) for e
in matrix
.to_3x3().to_euler())
173 tab_write(file, "camera {\n")
174 if scene
.pov
.baking_enable
and active_object
and active_object
.type == "MESH":
175 tab_write(file, "mesh_camera{ 1 3\n") # distribution 3 is what we want here
176 tab_write(file, "mesh{%s}\n" % active_object
.name
)
177 tab_write(file, "}\n")
178 tab_write(file, "location <0,0,.01>")
179 tab_write(file, "direction <0,0,-1>")
182 if camera
.data
.type == "ORTHO":
183 # XXX todo: track when SensorHeightRatio was added to see if needed (not used)
184 sensor_height_ratio
= (
185 render
.resolution_x
* camera
.data
.ortho_scale
/ render
.resolution_y
187 tab_write(file, "orthographic\n")
188 # Blender angle is radian so should be converted to degrees:
189 # % (camera.data.angle * (180.0 / pi) )
190 # but actually argument is not compulsory after angle in pov ortho mode
191 tab_write(file, "angle\n")
192 tab_write(file, "right <%6f, 0, 0>\n" % -camera
.data
.ortho_scale
)
193 tab_write(file, "location <0, 0, 0>\n")
194 tab_write(file, "look_at <0, 0, -1>\n")
195 tab_write(file, "up <0, %6f, 0>\n" % (camera
.data
.ortho_scale
/ q_size
))
197 elif camera
.data
.type == "PANO":
198 tab_write(file, "panoramic\n")
199 tab_write(file, "location <0, 0, 0>\n")
200 tab_write(file, "look_at <0, 0, -1>\n")
201 tab_write(file, "right <%s, 0, 0>\n" % -q_size
)
202 tab_write(file, "up <0, 1, 0>\n")
203 tab_write(file, "angle %f\n" % (360.0 * atan(16.0 / camera
.data
.lens
) / pi
))
204 elif camera
.data
.type == "PERSP":
205 # Standard camera otherwise would be default in pov
206 tab_write(file, "location <0, 0, 0>\n")
207 tab_write(file, "look_at <0, 0, -1>\n")
208 tab_write(file, "right <%s, 0, 0>\n" % -q_size
)
209 tab_write(file, "up <0, 1, 0>\n")
213 % (2 * atan(camera
.data
.sensor_width
/ 2 / camera
.data
.lens
) * 180.0 / pi
),
218 "rotate <%.6f, %.6f, %.6f>\n" % tuple(degrees(e
) for e
in matrix
.to_3x3().to_euler()),
221 tab_write(file, "translate <%.6f, %.6f, %.6f>\n" % matrix
.translation
[:])
222 if camera
.data
.dof
.use_dof
and (focal_point
!= 0 or camera
.data
.dof
.focus_object
):
224 file, "aperture %.3g\n" % (1 / (camera
.data
.dof
.aperture_fstop
* 10000) * 1000)
228 "blur_samples %d %d\n"
229 % (camera
.data
.pov
.dof_samples_min
, camera
.data
.pov
.dof_samples_max
),
231 tab_write(file, "variance 1/%d\n" % camera
.data
.pov
.dof_variance
)
232 tab_write(file, "confidence %.3g\n" % camera
.data
.pov
.dof_confidence
)
233 if camera
.data
.dof
.focus_object
:
234 focal_ob
= scene
.objects
[camera
.data
.dof
.focus_object
.name
]
235 matrix_blur
= global_matrix
@ focal_ob
.matrix_world
236 tab_write(file, "focal_point <%.4f,%.4f,%.4f>\n" % matrix_blur
.translation
[:])
238 tab_write(file, "focal_point <0, 0, %f>\n" % focal_point
)
239 if camera
.data
.pov
.normal_enable
:
242 "normal {%s %.4f turbulence %.4f scale %.4f}\n"
244 camera
.data
.pov
.normal_patterns
,
245 camera
.data
.pov
.cam_normal
,
246 camera
.data
.pov
.turbulence
,
247 camera
.data
.pov
.scale
,
250 tab_write(file, "}\n")
253 exported_lights_count
= 0
256 def export_lights(lamps
, file, scene
, global_matrix
, tab_write
):
257 """Translate lights from Blender UI to POV syntax and write to exported file."""
259 from .render
import write_matrix
, tab_write
261 # Incremented after each lamp export to declare its target
262 # currently used for Fresnel diffuse shader as their slope vector:
263 global exported_lights_count
264 # Get all lamps and keep their count in a global variable
265 for exported_lights_count
, ob
in enumerate(lamps
, start
=1):
268 matrix
= global_matrix
@ ob
.matrix_world
270 # Color is no longer modified by energy
271 # any way to directly get bpy_prop_array as tuple?
272 color
= tuple(lamp
.color
)
274 tab_write(file, "light_source {\n")
275 tab_write(file, "< 0,0,0 >\n")
276 tab_write(file, "color srgb<%.3g, %.3g, %.3g>\n" % color
)
278 if lamp
.type == "POINT":
280 elif lamp
.type == "SPOT":
281 tab_write(file, "spotlight\n")
283 # Falloff is the main radius from the centre line
284 tab_write(file, "falloff %.2f\n" % (degrees(lamp
.spot_size
) / 2.0)) # 1 TO 179 FOR BOTH
286 file, "radius %.6f\n" % ((degrees(lamp
.spot_size
) / 2.0) * (1.0 - lamp
.spot_blend
))
289 # Blender does not have a tightness equivalent, 0 is most like blender default.
290 tab_write(file, "tightness 0\n") # 0:10f
292 tab_write(file, "point_at <0, 0, -1>\n")
293 if lamp
.pov
.use_halo
:
294 tab_write(file, "looks_like{\n")
295 tab_write(file, "sphere{<0,0,0>,%.6f\n" % lamp
.distance
)
296 tab_write(file, "hollow\n")
297 tab_write(file, "material{\n")
298 tab_write(file, "texture{\n")
299 tab_write(file, "pigment{rgbf<1,1,1,%.4f>}\n" % (lamp
.pov
.halo_intensity
* 5.0))
300 tab_write(file, "}\n")
301 tab_write(file, "interior{\n")
302 tab_write(file, "media{\n")
303 tab_write(file, "emission 1\n")
304 tab_write(file, "scattering {1, 0.5}\n")
305 tab_write(file, "density{\n")
306 tab_write(file, "spherical\n")
307 tab_write(file, "color_map{\n")
308 tab_write(file, "[0.0 rgb <0,0,0>]\n")
309 tab_write(file, "[0.5 rgb <1,1,1>]\n")
310 tab_write(file, "[1.0 rgb <1,1,1>]\n")
311 tab_write(file, "}\n")
312 tab_write(file, "}\n")
313 tab_write(file, "}\n")
314 tab_write(file, "}\n")
315 tab_write(file, "}\n")
316 tab_write(file, "}\n")
317 tab_write(file, "}\n")
318 elif lamp
.type == "SUN":
319 tab_write(file, "parallel\n")
320 tab_write(file, "point_at <0, 0, -1>\n") # *must* be after 'parallel'
322 elif lamp
.type == "AREA":
323 tab_write(file, "fade_distance %.6f\n" % (lamp
.distance
/ 2.0))
324 # Area lights have no falloff type, so always use blenders lamp quad equivalent
326 tab_write(file, "fade_power %d\n" % 2)
328 samples_x
= lamp
.pov
.shadow_ray_samples_x
329 if lamp
.shape
== "SQUARE":
331 samples_y
= samples_x
334 samples_y
= lamp
.pov
.shadow_ray_samples_y
338 "area_light <%.6f,0,0>,<0,%.6f,0> %d, %d\n"
339 % (size_x
, size_y
, samples_x
, samples_y
),
341 tab_write(file, "area_illumination\n")
342 if lamp
.pov
.shadow_ray_sample_method
== "CONSTANT_JITTERED":
343 if lamp
.pov
.use_jitter
:
344 tab_write(file, "jitter\n")
346 tab_write(file, "adaptive 1\n")
347 tab_write(file, "jitter\n")
349 # No shadow checked either at global or light level:
350 if not scene
.pov
.use_shadows
or (lamp
.pov
.shadow_method
== "NOSHADOW"):
351 tab_write(file, "shadowless\n")
353 # Sun shouldn't be attenuated. Area lights have no falloff attribute so they
354 # are put to type 2 attenuation a little higher above.
355 if lamp
.type not in {"SUN", "AREA"}:
356 if lamp
.falloff_type
== "INVERSE_SQUARE":
357 tab_write(file, "fade_distance %.6f\n" % (sqrt(lamp
.distance
/ 2.0)))
358 tab_write(file, "fade_power %d\n" % 2) # Use blenders lamp quad equivalent
359 elif lamp
.falloff_type
== "INVERSE_LINEAR":
360 tab_write(file, "fade_distance %.6f\n" % (lamp
.distance
/ 2.0))
361 tab_write(file, "fade_power %d\n" % 1) # Use blenders lamp linear
362 elif lamp
.falloff_type
== "CONSTANT":
363 tab_write(file, "fade_distance %.6f\n" % (lamp
.distance
/ 2.0))
364 tab_write(file, "fade_power %d\n" % 3)
365 # Use blenders lamp constant equivalent no attenuation.
366 # Using Custom curve for fade power 3 for now.
367 elif lamp
.falloff_type
== "CUSTOM_CURVE":
368 tab_write(file, "fade_power %d\n" % 4)
370 write_matrix(file, matrix
)
372 tab_write(file, "}\n")
374 # v(A,B) rotates vector A about origin by vector B.
376 "#declare lampTarget%s= vrotate(<%.4g,%.4g,%.4g>,<%.4g,%.4g,%.4g>);\n"
378 exported_lights_count
,
389 def export_world(file, world
, scene
, global_matrix
, tab_write
):
390 """write world as POV background and sky_sphere to exported file"""
392 agnosticrender
= scene
.render
393 camera
= scene
.camera
394 # matrix = global_matrix @ camera.matrix_world # view dependant for later use NOT USED
398 # These lines added to get sky gradient (visible with PNG output)
400 # For simple flat background:
401 if not world
.pov
.use_sky_blend
:
402 # No alpha with Sky option:
403 if render
.alpha_mode
== "SKY" and not agnosticrender
.film_transparent
:
405 file, "background {rgbt<%.3g, %.3g, %.3g, 0>}\n" % (world
.pov
.horizon_color
[:])
408 elif render
.alpha_mode
== "STRAIGHT" or agnosticrender
.film_transparent
:
410 file, "background {rgbt<%.3g, %.3g, %.3g, 1>}\n" % (world
.pov
.horizon_color
[:])
413 # Non fully transparent background could premultiply alpha and avoid
414 # anti-aliasing display issue
417 "background {rgbft<%.3g, %.3g, %.3g, %.3g, 0>}\n"
419 world
.pov
.horizon_color
[0],
420 world
.pov
.horizon_color
[1],
421 world
.pov
.horizon_color
[2],
427 # For Background image textures
428 for t
in world
.pov_texture_slots
: # risk to write several sky_spheres but maybe ok.
430 tex
= bpy
.data
.textures
[t
.texture
]
431 if tex
.type is not None:
433 # XXX No enable checkbox for world textures yet (report it?)
434 # if t and tex.type == 'IMAGE' and t.use:
435 if tex
.type == "IMAGE":
436 image_filename
= path_image(tex
.image
)
437 if tex
.image
.filepath
!= image_filename
:
438 tex
.image
.filepath
= image_filename
439 if image_filename
!= "" and t
.use_map_blend
:
440 textures_blend
= image_filename
441 # colvalue = t.default_value
444 # Commented below was an idea to make the Background image oriented as camera
446 # http://news.pov.org/pov.newusers/thread/%3Cweb.4a5cddf4e9c9822ba2f93e20@news.pov.org%3E/
447 # Replace 4/3 by the ratio of each image found by some custom or existing
449 # mapping_blend = (" translate <%.4g,%.4g,%.4g> rotate z*degrees" \
450 # "(atan((camLocation - camLookAt).x/(camLocation - " \
451 # "camLookAt).y)) rotate x*degrees(atan((camLocation - " \
452 # "camLookAt).y/(camLocation - camLookAt).z)) rotate y*" \
453 # "degrees(atan((camLocation - camLookAt).z/(camLocation - " \
454 # "camLookAt).x)) scale <%.4g,%.4g,%.4g>b" % \
455 # (t_blend.offset.x / 10 , t_blend.offset.y / 10 ,
456 # t_blend.offset.z / 10, t_blend.scale.x ,
457 # t_blend.scale.y , t_blend.scale.z))
458 # using camera rotation valuesdirectly from blender seems much easier
459 if t_blend
.texture_coords
== "ANGMAP":
462 # POV-Ray "scale" is not a number of repetitions factor, but its
463 # inverse, a standard scale factor.
464 # 0.5 Offset is needed relatively to scale because center of the
465 # UV scale is 0.5,0.5 in blender and 0,0 in POV
466 # Further Scale by 2 and translate by -1 are
467 # required for the sky_sphere not to repeat
470 "scale 2 scale <%.4g,%.4g,%.4g> translate -1 "
471 "translate <%.4g,%.4g,%.4g> rotate<0,0,0> "
473 (1.0 / t_blend
.scale
.x
),
474 (1.0 / t_blend
.scale
.y
),
475 (1.0 / t_blend
.scale
.z
),
476 0.5 - (0.5 / t_blend
.scale
.x
) - t_blend
.offset
.x
,
477 0.5 - (0.5 / t_blend
.scale
.y
) - t_blend
.offset
.y
,
482 # The initial position and rotation of the pov camera is probably creating
483 # the rotation offset should look into it someday but at least background
484 # won't rotate with the camera now.
485 # Putting the map on a plane would not introduce the skysphere distortion and
486 # allow for better image scale matching but also some waay to chose depth and
487 # size of the plane relative to camera.
488 tab_write(file, "sky_sphere {\n")
489 tab_write(file, "pigment {\n")
492 'image_map{%s "%s" %s}\n'
493 % (image_format(textures_blend
), textures_blend
, img_map_bg(t_blend
)),
495 tab_write(file, "}\n")
496 tab_write(file, "%s\n" % mapping_blend
)
497 # The following layered pigment opacifies to black over the texture for
498 # transmit below 1 or otherwise adds to itself
499 tab_write(file, "pigment {rgb 0 transmit %s}\n" % tex
.intensity
)
500 tab_write(file, "}\n")
501 # tab_write(file, "scale 2\n")
502 # tab_write(file, "translate -1\n")
504 # For only Background gradient
506 if world_tex_count
== 0 and world
.pov
.use_sky_blend
:
507 tab_write(file, "sky_sphere {\n")
508 tab_write(file, "pigment {\n")
509 # maybe Should follow the advice of POV doc about replacing gradient
511 tab_write(file, "gradient y\n")
512 tab_write(file, "color_map {\n")
514 if render
.alpha_mode
== "TRANSPARENT":
517 "[0.0 rgbft<%.3g, %.3g, %.3g, %.3g, 0>]\n"
519 world
.pov
.horizon_color
[0],
520 world
.pov
.horizon_color
[1],
521 world
.pov
.horizon_color
[2],
527 "[1.0 rgbft<%.3g, %.3g, %.3g, %.3g, 0>]\n"
529 world
.pov
.zenith_color
[0],
530 world
.pov
.zenith_color
[1],
531 world
.pov
.zenith_color
[2],
535 if agnosticrender
.film_transparent
or render
.alpha_mode
== "STRAIGHT":
536 tab_write(file, "[0.0 rgbt<%.3g, %.3g, %.3g, 0.99>]\n" % (world
.pov
.horizon_color
[:]))
537 # aa premult not solved with transmit 1
538 tab_write(file, "[1.0 rgbt<%.3g, %.3g, %.3g, 0.99>]\n" % (world
.pov
.zenith_color
[:]))
540 tab_write(file, "[0.0 rgbt<%.3g, %.3g, %.3g, 0>]\n" % (world
.pov
.horizon_color
[:]))
541 tab_write(file, "[1.0 rgbt<%.3g, %.3g, %.3g, 0>]\n" % (world
.pov
.zenith_color
[:]))
542 tab_write(file, "}\n")
543 tab_write(file, "}\n")
544 tab_write(file, "}\n")
545 # Sky_sphere alpha (transmit) is not translating into image alpha the same
546 # way as 'background'
548 # if world.pov.light_settings.use_indirect_light:
549 # scene.pov.radio_enable=1
551 # Maybe change the above to a function copyInternalRenderer settings when
552 # user pushes a button, then:
553 # scene.pov.radio_enable = world.pov.light_settings.use_indirect_light
554 # and other such translations but maybe this would not be allowed either?
556 # -----------------------------------------------------------------------------
558 mist
= world
.mist_settings
561 tab_write(file, "fog {\n")
562 if mist
.falloff
== "LINEAR":
563 tab_write(file, "distance %.6f\n" % ((mist
.start
+ mist
.depth
) * 0.368))
564 elif mist
.falloff
in ["QUADRATIC", "INVERSE_QUADRATIC"]: # n**2 or squrt(n)?
565 tab_write(file, "distance %.6f\n" % ((mist
.start
+ mist
.depth
) ** 2 * 0.368))
568 "color rgbt<%.3g, %.3g, %.3g, %.3g>\n"
569 % (*world
.pov
.horizon_color
, (1.0 - mist
.intensity
)),
571 # tab_write(file, "fog_offset %.6f\n" % mist.start) #create a pov property to prepend
572 # tab_write(file, "fog_alt %.6f\n" % mist.height) #XXX right?
573 # tab_write(file, "turbulence 0.2\n")
574 # tab_write(file, "turb_depth 0.3\n")
575 tab_write(file, "fog_type 1\n") # type2 for height
576 tab_write(file, "}\n")
577 if scene
.pov
.media_enable
:
578 tab_write(file, "media {\n")
581 "scattering { %d, rgb %.12f*<%.4g, %.4g, %.4g>\n"
583 int(scene
.pov
.media_scattering_type
),
584 scene
.pov
.media_diffusion_scale
,
585 *(scene
.pov
.media_diffusion_color
[:]),
588 if scene
.pov
.media_scattering_type
== "5":
589 tab_write(file, "eccentricity %.3g\n" % scene
.pov
.media_eccentricity
)
590 tab_write(file, "}\n")
593 "absorption %.12f*<%.4g, %.4g, %.4g>\n"
594 % (scene
.pov
.media_absorption_scale
, *(scene
.pov
.media_absorption_color
[:])),
596 tab_write(file, "\n")
597 tab_write(file, "samples %.d\n" % scene
.pov
.media_samples
)
598 tab_write(file, "}\n")
601 # -----------------------------------------------------------------------------
602 def export_rainbows(rainbows
, file, scene
, global_matrix
, tab_write
):
603 """write all POV rainbows primitives to exported file"""
605 from .render
import write_matrix
, tab_write
607 pov_mat_name
= "Default_texture"
609 povdataname
= ob
.data
.name
# enough? XXX not used nor matrix fn?
610 angle
= degrees(ob
.data
.spot_size
/ 2.5) # radians in blender (2
611 width
= ob
.data
.spot_blend
* 10
612 distance
= ob
.data
.shadow_buffer_clip_start
614 # angle = br/(cr+eps) * 10 #eps is small epsilon variable to avoid dividing by zero
615 # width = ob.dimensions[2] #now let's say width of rainbow is the actual proxy height
617 # cz-bz # let's say width of the rainbow is height of the cone (interfacing choice
619 # v(A,B) rotates vector A about origin by vector B.
620 # and avoid a 0 length vector by adding 1
622 # file.write("#declare %s_Target= vrotate(<%.6g,%.6g,%.6g>,<%.4g,%.4g,%.4g>);\n" % \
623 # (povdataname, -(ob.location.x+0.1), -(ob.location.y+0.1), -(ob.location.z+0.1),
624 # ob.rotation_euler.x, ob.rotation_euler.y, ob.rotation_euler.z))
626 direction
= ( # XXX currently not used (replaced by track to?)
630 ) # not taking matrix into account
631 rmatrix
= global_matrix
@ ob
.matrix_world
633 # ob.rotation_euler.to_matrix().to_4x4() * mathutils.Vector((0,0,1))
634 # XXX Is result of the below offset by 90 degrees?
635 up
= ob
.matrix_world
.to_3x3()[1].xyz
# * global_matrix
639 # tab_write(file, "#declare %s = rainbow {\n"%povdataname)
641 # clumsy for now but remove the rainbow from instancing
642 # system because not an object. use lamps later instead of meshes
644 # del data_ref[dataname]
645 tab_write(file, "rainbow {\n")
647 tab_write(file, "angle %.4f\n" % angle
)
648 tab_write(file, "width %.4f\n" % width
)
649 tab_write(file, "distance %.4f\n" % distance
)
650 tab_write(file, "arc_angle %.4f\n" % ob
.pov
.arc_angle
)
651 tab_write(file, "falloff_angle %.4f\n" % ob
.pov
.falloff_angle
)
652 tab_write(file, "direction <%.4f,%.4f,%.4f>\n" % rmatrix
.translation
[:])
653 tab_write(file, "up <%.4f,%.4f,%.4f>\n" % (up
[0], up
[1], up
[2]))
654 tab_write(file, "color_map {\n")
655 tab_write(file, "[0.000 color srgbt<1.0, 0.5, 1.0, 1.0>]\n")
656 tab_write(file, "[0.130 color srgbt<0.5, 0.5, 1.0, 0.9>]\n")
657 tab_write(file, "[0.298 color srgbt<0.2, 0.2, 1.0, 0.7>]\n")
658 tab_write(file, "[0.412 color srgbt<0.2, 1.0, 1.0, 0.4>]\n")
659 tab_write(file, "[0.526 color srgbt<0.2, 1.0, 0.2, 0.4>]\n")
660 tab_write(file, "[0.640 color srgbt<1.0, 1.0, 0.2, 0.4>]\n")
661 tab_write(file, "[0.754 color srgbt<1.0, 0.5, 0.2, 0.6>]\n")
662 tab_write(file, "[0.900 color srgbt<1.0, 0.2, 0.2, 0.7>]\n")
663 tab_write(file, "[1.000 color srgbt<1.0, 0.2, 0.2, 1.0>]\n")
664 tab_write(file, "}\n")
666 # tab_write(file, "texture {%s}\n"%pov_mat_name)
667 write_object_modifiers(ob
, file)
668 # tab_write(file, "rotate x*90\n")
669 # matrix = global_matrix @ ob.matrix_world
670 # write_matrix(file, matrix)
671 tab_write(file, "}\n")
672 # continue #Don't render proxy mesh, skip to next object
675 def export_smoke(file, smoke_obj_name
, smoke_path
, comments
, global_matrix
):
676 """export Blender smoke type fluids to pov media using df3 library"""
678 from .render
import write_matrix
, tab_write
680 flowtype
= -1 # XXX todo: not used yet? should trigger emissive for fire type
681 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
682 smoke_obj
= bpy
.data
.objects
[smoke_obj_name
].evaluated_get(depsgraph
)
684 smoke_modifier
= None
685 # Search smoke domain target for smoke modifiers
686 for mod
in smoke_obj
.modifiers
:
687 if mod
.type == "FLUID":
688 if mod
.fluid_type
== "DOMAIN":
692 elif mod
.fluid_type
== "FLOW":
693 if mod
.flow_settings
.flow_type
== "BOTH":
695 elif mod
.flow_settings
.flow_type
== "FIRE":
697 elif mod
.flow_settings
.flow_type
== "SMOKE":
699 eps
= 0.000001 # XXX not used currently. restore from corner case ... zero div?
700 if domain
is not None:
701 mod_set
= smoke_modifier
.domain_settings
703 for v
in mod_set
.density_grid
:
704 channeldata
.append(v
.real
)
706 # -------- Usage in voxel texture:
708 # if channel == 'density':
709 # for v in mod_set.density_grid:
710 # channeldata.append(v.real)
712 # if channel == 'fire':
713 # for v in mod_set.flame_grid:
714 # channeldata.append(v.real)
716 resolution
= mod_set
.resolution_max
718 mod_set
.domain_resolution
[0],
719 mod_set
.domain_resolution
[1],
720 mod_set
.domain_resolution
[2],
723 if mod_set
.use_noise
:
724 big_res
[0] = big_res
[0] * (mod_set
.noise_scale
+ 1)
725 big_res
[1] = big_res
[1] * (mod_set
.noise_scale
+ 1)
726 big_res
[2] = big_res
[2] * (mod_set
.noise_scale
+ 1)
729 # -------- gather smoke domain settings
730 # BBox = domain.bound_box
731 # p.append([BBox[0][0], BBox[0][1], BBox[0][2]])
732 # p.append([BBox[6][0], BBox[6][1], BBox[6][2]])
733 # mod_set = smoke_modifier.domain_settings
734 # resolution = mod_set.resolution_max
735 # smokecache = mod_set.point_cache
736 # ret = read_cache(smokecache, mod_set.use_noise, mod_set.noise_scale + 1, flowtype)
743 # if res_x * res_y * res_z > 0:
744 # -------- new cache format
746 # big_res.append(res_x)
747 # big_res.append(res_y)
748 # big_res.append(res_z)
750 # max = domain.dimensions[0]
751 # if (max - domain.dimensions[1]) < -eps:
752 # max = domain.dimensions[1]
754 # if (max - domain.dimensions[2]) < -eps:
755 # max = domain.dimensions[2]
757 # big_res = [int(round(resolution * domain.dimensions[0] / max, 0)),
758 # int(round(resolution * domain.dimensions[1] / max, 0)),
759 # int(round(resolution * domain.dimensions[2] / max, 0))]
761 # if mod_set.use_noise:
762 # big_res = [big_res[0] * (mod_set.noise_scale + 1),
763 # big_res[1] * (mod_set.noise_scale + 1),
764 # big_res[2] * (mod_set.noise_scale + 1)]
766 # if channel == 'density':
767 # channeldata = density
769 # if channel == 'fire':
772 # sc_fr = '%s/%s/%s/%05d' % (
773 # efutil.export_path,
774 # efutil.scene_filename(),
775 # bpy.context.scene.name,
776 # bpy.context.scene.frame_current
778 # if not os.path.exists( sc_fr ):
781 # smoke_filename = '%s.smoke' % bpy.path.clean_name(domain.name)
782 # smoke_path = '/'.join([sc_fr, smoke_filename])
784 # with open(smoke_path, 'wb') as smoke_file:
785 # # Binary densitygrid file format
788 # smoke_file.write(b'SMOKE') #magic number
789 # smoke_file.write(struct.pack('<I', big_res[0]))
790 # smoke_file.write(struct.pack('<I', big_res[1]))
791 # smoke_file.write(struct.pack('<I', big_res[2]))
793 # smoke_file.write(struct.pack('<%df'%len(channeldata), *channeldata))
795 # LuxLog('Binary SMOKE file written: %s' % (smoke_path))
797 # return big_res[0], big_res[1], big_res[2], channeldata
799 mydf3
= voxel_lib
.df3(big_res
[0], big_res
[1], big_res
[2])
800 sim_sizeX
, sim_sizeY
, sim_sizeZ
= mydf3
.size()
801 for x
in range(sim_sizeX
):
802 for y
in range(sim_sizeY
):
803 for z
in range(sim_sizeZ
):
804 mydf3
.set(x
, y
, z
, channeldata
[((z
* sim_sizeY
+ y
) * sim_sizeX
+ x
)])
806 mydf3
.exportDF3(smoke_path
)
807 except ZeroDivisionError:
808 print("Show smoke simulation in 3D view before export")
809 print("Binary smoke.df3 file written in preview directory")
811 file.write("\n//--Smoke--\n\n")
813 # Note: We start with a default unit cube.
814 # This is mandatory to read correctly df3 data - otherwise we could just directly use
815 # bbox coordinates from the start, and avoid scale/translate operations at the end...
816 file.write("box{<0,0,0>, <1,1,1>\n")
817 file.write(" pigment{ rgbt 1 }\n")
818 file.write(" hollow\n")
819 file.write(" interior{ //---------------------\n")
820 file.write(" media{ method 3\n")
821 file.write(" emission <1,1,1>*1\n") # 0>1 for dark smoke to white vapour
822 file.write(" scattering{ 1, // Type\n")
823 file.write(" <1,1,1>*0.1\n")
824 file.write(" } // end scattering\n")
825 file.write(' density{density_file df3 "%s"\n' % smoke_path
)
826 file.write(" color_map {\n")
827 file.write(" [0.00 rgb 0]\n")
828 file.write(" [0.05 rgb 0]\n")
829 file.write(" [0.20 rgb 0.2]\n")
830 file.write(" [0.30 rgb 0.6]\n")
831 file.write(" [0.40 rgb 1]\n")
832 file.write(" [1.00 rgb 1]\n")
833 file.write(" } // end color_map\n")
834 file.write(" } // end of density\n")
835 file.write(" samples %i // higher = more precise\n" % resolution
)
836 file.write(" } // end of media --------------------------\n")
837 file.write(" } // end of interior\n")
839 # START OF TRANSFORMATIONS
841 # Size to consider here are bbox dimensions (i.e. still in object space, *before* applying
842 # loc/rot/scale and other transformations (like parent stuff), aka matrix_world).
843 bbox
= smoke_obj
.bound_box
845 abs(bbox
[6][0] - bbox
[0][0]),
846 abs(bbox
[6][1] - bbox
[0][1]),
847 abs(bbox
[6][2] - bbox
[0][2]),
850 # We scale our cube to get its final size and shapes but still in *object* space (same as Blender's bbox).
851 file.write("scale<%.6g,%.6g,%.6g>\n" % (dim
[0], dim
[1], dim
[2]))
853 # We offset our cube such that (0,0,0) coordinate matches Blender's object center.
854 file.write("translate<%.6g,%.6g,%.6g>\n" % (bbox
[0][0], bbox
[0][1], bbox
[0][2]))
856 # We apply object's transformations to get final loc/rot/size in world space!
857 # Note: we could combine the two previous transformations with this matrix directly...
858 write_matrix(file, global_matrix
@ smoke_obj
.matrix_world
)
860 # END OF TRANSFORMATIONS
864 # file.write(" interpolate 1\n")
865 # file.write(" frequency 0\n")