1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
20 "name": "Import GIMP Image to Scene (.xcf/.xjt)",
21 "author": "Daniel Salazar (ZanQdo)",
23 "blender": (2, 73, 0),
24 "location": "File > Import > GIMP Image to Scene(.xcf/.xjt)",
25 "description": "Imports GIMP multilayer image files as a series of multiple planes",
26 "warning": "XCF import requires xcftools installed",
27 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Import-Export/GIMPImageToScene",
29 "category": "Import-Export",
33 This script imports GIMP layered image files into 3D Scenes (.xcf, .xjt)
36 def main(report
, File
, Path
, LayerViewers
, MixerViewers
, LayerOffset
,
37 LayerScale
, OpacityMode
, AlphaMode
, ShadelessMats
,
38 SetCamera
, SetupCompo
, GroupUntagged
, Ext
):
40 #-------------------------------------------------
42 #Folder = '['+File.rstrip(Ext)+']'+'_images/'
43 Folder
= 'images_'+'['+File
.rstrip(Ext
)+']/'
45 if not bpy
.data
.is_saved
:
46 PathSaveRaw
= Path
+Folder
47 PathSave
= PathSaveRaw
.replace(' ', '\ ')
48 try: os
.mkdir(PathSaveRaw
)
51 PathSave
= bpy
.data
.filepath
52 RSlash
= PathSave
.rfind('/')
53 PathSaveRaw
= PathSave
[:RSlash
+1]+Folder
54 PathSave
= PathSaveRaw
.replace(' ', '\ ')
55 try: os
.mkdir(PathSaveRaw
)
57 PathSaveRaw
= bpy
.path
.relpath(PathSaveRaw
)+'/'
60 Path
= Path
.replace(' ', '\ ')
63 #-------------------------------------------------
67 IMG
= tarfile
.open ('%s%s' % (PathRaw
, File
))
68 PRP
= IMG
.extractfile('PRP')
70 Members
= IMG
.getmembers()
72 for Member
in Members
:
74 if Name
.startswith('l') and Name
.endswith('.jpg'):
75 IMG
.extract(Name
, path
=PathSaveRaw
)
77 #-------------------------------------------------
80 for Line
in PRP
.readlines():
83 if Line
.startswith("b'GIMP_XJ_IMAGE"):
84 for Segment
in Line
.split():
85 if Segment
.startswith('w/h:'):
86 ResX
, ResY
= map (int, Segment
[4:].split(','))
87 if Line
.startswith(("b'L", "b'l")):
89 """The "nice" method to check if layer has alpha channel
90 sadly GIMP sometimes decides not to export an alpha channel
91 if it's pure white so we are not completly sure here yet"""
92 if Line
.startswith("b'L"): HasAlpha
= True
93 else: HasAlpha
= False
99 for Segment
in Line
.split():
101 if Segment
.startswith("b'"):
102 imageFile
= 'l' + Segment
[3:] + '.jpg'
103 imageFileAlpha
='la'+Segment
[3:]+'.jpg'
105 """Phisically double checking if alpha image exists
106 now we can be sure! (damn GIMP)"""
108 if not os
.path
.isfile(PathSaveRaw
+imageFileAlpha
): HasAlpha
= False
110 # Get Widht and Height from images
111 data
= open(PathSaveRaw
+imageFile
, "rb").read()
118 for k
in range(len(hexList
)-1):
119 if hexList
[k
] == 'FF' and (hexList
[k
+1] == 'C0' or hexList
[k
+1] == 'C2'):
120 ow
= int(hexList
[k
+7],16)*256 + int(hexList
[k
+8],16)
121 oh
= int(hexList
[k
+5],16)*256 + int(hexList
[k
+6],16)
123 elif Segment
.startswith('md:'): # mode
126 elif Segment
.startswith('op:'): # opacity
127 op
= float(Segment
[3:])*.01
129 elif Segment
.startswith('o:'): # origin
130 ox
, oy
= map(int, Segment
[2:].split(','))
132 elif Segment
.startswith('n:'): # name
134 OpenBracket
= n
.find ('[')
135 CloseBracket
= n
.find (']')
137 if OpenBracket
!= -1 and CloseBracket
!= -1:
138 RenderLayer
= n
[OpenBracket
+1:CloseBracket
]
139 NameShort
= n
[:OpenBracket
]
145 os
.rename(PathSaveRaw
+imageFile
, PathSaveRaw
+NameShort
+'.jpg')
146 if HasAlpha
: os
.rename(PathSaveRaw
+imageFileAlpha
, PathSaveRaw
+NameShort
+'_A'+'.jpg')
148 IMGs
.append({'LayerMode':md
, 'LayerOpacity':op
,
149 'LayerName':n
, 'LayerNameShort':NameShort
,
150 'RenderLayer':RenderLayer
, 'LayerCoords':[ow
, oh
, ox
, oy
], 'HasAlpha':HasAlpha
})
152 else: # Ext == '.xcf':
154 #-------------------------------------------------
158 #-------------------------------------------------
162 Info
= subprocess
.check_output((XCFInfo
, Path
+File
))
163 except FileNotFoundError
as e
:
164 if XCFInfo
in str(e
):
165 report({'ERROR'}, "Please install xcftools, xcfinfo seems to be missing (%s)" % str(e
))
172 for Line
in Info
.split('\n'):
173 if Line
.startswith ('+'):
175 Line
= Line
.split(' ', 4)
177 RenderLayer
= Line
[4]
179 OpenBracket
= RenderLayer
.find ('[')
180 CloseBracket
= RenderLayer
.find (']')
182 if OpenBracket
!= -1 and CloseBracket
!= -1:
183 RenderLayer
= RenderLayer
[OpenBracket
+1:CloseBracket
]
184 NameShort
= Line
[4][:OpenBracket
]
186 NameShort
= Line
[4].rstrip()
188 RenderLayer
= '__Undefined__'
190 RenderLayer
= NameShort
193 Slash
= LineThree
.find('/')
198 Mode
= LineThree
[:Slash
]
199 Opacity
= float(LineThree
[Slash
+1:LineThree
.find('%')])*.01
203 'LayerOpacity': Opacity
,
204 'LayerName': Line
[4].rstrip(),
205 'LayerNameShort': NameShort
,
206 'LayerCoords': list(map(int, Line
[1].replace('x', ' ').replace('+', ' +').replace('-', ' -').split())),
207 'RenderLayer': RenderLayer
,
210 elif Line
.startswith('Version'):
211 ResX
, ResY
= map (int, Line
.split()[2].split('x'))
213 #-------------------------------------------------
215 if OpacityMode
== 'BAKE':
218 Opacity
= ("--percent", "100")
219 xcf_path
= Path
+ File
221 png_path
= "%s%s.png" % (PathSave
, Layer
['LayerName'].replace(' ', '_'))
222 subprocess
.call((XCF2PNG
, "-C", xcf_path
, "-o", png_path
, Layer
['LayerName']) + Opacity
)
224 #-------------------------------------------------
225 Scene
= bpy
.context
.scene
226 #-------------------------------------------------
230 bpy
.ops
.object.camera_add(location
=(0, 0, 10))
232 Camera
= bpy
.context
.active_object
.data
234 Camera
.type = 'ORTHO'
235 Camera
.ortho_scale
= ResX
* .01
237 #-------------------------------------------------
240 Render
= Scene
.render
243 Render
.resolution_x
= ResX
244 Render
.resolution_y
= ResY
245 Render
.resolution_percentage
= 100
246 Render
.alpha_mode
= 'TRANSPARENT'
248 #-------------------------------------------------
251 Scene
.game_settings
.material_mode
= 'GLSL'
253 Areas
= bpy
.context
.screen
.areas
256 if Area
.type == 'VIEW_3D':
257 Area
.spaces
.active
.viewport_shade
= 'TEXTURED'
258 Area
.spaces
.active
.show_textured_solid
= True
259 Area
.spaces
.active
.show_floor
= False
261 #-------------------------------------------------
264 def Make3DLayer (Name
, NameShort
, Z
, Coords
, RenderLayer
, LayerMode
, LayerOpacity
, HasAlpha
):
269 if not bpy
.context
.scene
.render
.layers
.get(RenderLayer
):
271 bpy
.ops
.scene
.render_layer_add()
273 LayerActive
= bpy
.context
.scene
.render
.layers
.active
274 LayerActive
.name
= RenderLayer
275 LayerActive
.use_pass_vector
= True
276 LayerActive
.use_sky
= False
277 LayerActive
.use_edge_enhance
= False
278 LayerActive
.use_strand
= False
279 LayerActive
.use_halo
= False
282 for i
in range (0,20):
283 if not i
== LayerNum
:
284 LayerActive
.layers
[i
] = False
286 bpy
.context
.scene
.layers
[LayerNum
] = True
288 LayerFlags
[RenderLayer
] = bpy
.context
.scene
.render
.layers
.active
.layers
290 LayerList
.append([RenderLayer
, LayerMode
, LayerOpacity
])
295 bpy
.ops
.mesh
.primitive_plane_add(view_align
=False,
296 enter_editmode
=False,
299 bpy
.ops
.object.transform_apply(location
=False, rotation
=True, scale
=False)
302 Active
= bpy
.context
.active_object
305 Active
.layers
= LayerFlags
[RenderLayer
]
308 (float(Coords
[2])-(ResX
*0.5))*LayerScale
,
309 (-float(Coords
[3])+(ResY
*0.5))*LayerScale
, Z
)
311 for Vert
in Active
.data
.vertices
:
315 Active
.dimensions
= float(Coords
[0])*LayerScale
, float(Coords
[1])*LayerScale
, 0
317 bpy
.ops
.object.transform_apply(location
=False, rotation
=False, scale
=True)
319 bpy
.ops
.object.origin_set(type='ORIGIN_GEOMETRY', center
='MEDIAN')
321 Active
.show_wire
= True
323 Active
.name
= NameShort
324 bpy
.ops
.mesh
.uv_texture_add()
328 '''if bpy.data.materials.get(NameShort):
329 Mat = bpy.data.materials[NameShort]
330 if not Active.material_slots:
331 bpy.ops.object.material_slot_add()
332 Active.material_slots[0].material = Mat
335 Mat
= bpy
.data
.materials
.new(NameShort
)
336 Mat
.diffuse_color
= (1,1,1)
337 Mat
.use_raytrace
= False
338 Mat
.use_shadows
= False
339 Mat
.use_cast_buffer_shadows
= False
340 Mat
.use_cast_approximate
= False
342 Mat
.use_transparency
= True
343 if OpacityMode
== 'MAT': Mat
.alpha
= LayerOpacity
345 if ShadelessMats
: Mat
.use_shadeless
= True
349 Tex
= bpy
.data
.textures
.new(NameShort
, 'IMAGE')
350 Tex
.extension
= 'CLIP'
351 Tex
.use_preview_alpha
= True
353 Img
= bpy
.data
.images
.new(NameShort
, 128, 128)
355 Img
.alpha_mode
= AlphaMode
356 Img
.filepath
= '%s%s%s' % (PathSaveRaw
, Name
, ExtSave
)
358 UVFace
= Active
.data
.uv_textures
[0].data
[0]
363 Mat
.texture_slots
.add()
364 TexSlot
= Mat
.texture_slots
[0]
365 TexSlot
.texture
= Tex
366 TexSlot
.use_map_alpha
= True
367 TexSlot
.texture_coords
= 'UV'
368 if OpacityMode
== 'TEX': TexSlot
.alpha_factor
= LayerOpacity
369 elif OpacityMode
== 'MAT': TexSlot
.blend_type
= 'MULTIPLY'
371 else: # Ext == '.xjt'
373 Tex
= bpy
.data
.textures
.new(NameShort
, 'IMAGE')
374 Tex
.extension
= 'CLIP'
376 Img
= bpy
.data
.images
.new(NameShort
, 128, 128)
378 Img
.filepath
= '%s%s%s' % (PathSaveRaw
, Name
, ExtSave
)
380 UVFace
= Active
.data
.uv_textures
[0].data
[0]
385 Mat
.texture_slots
.add()
386 TexSlot
= Mat
.texture_slots
[0]
387 TexSlot
.texture
= Tex
388 TexSlot
.texture_coords
= 'UV'
392 Tex
= bpy
.data
.textures
.new(NameShort
+'_A', 'IMAGE')
393 Tex
.extension
= 'CLIP'
394 Tex
.use_preview_alpha
= True
396 Img
= bpy
.data
.images
.new(NameShort
+'_A', 128, 128)
398 Img
.alpha_mode
= AlphaMode
399 Img
.filepath
= '%s%s_A%s' % (PathSaveRaw
, Name
, ExtSave
)
400 Img
.use_alpha
= False
404 Mat
.texture_slots
.add()
405 TexSlot
= Mat
.texture_slots
[1]
406 TexSlot
.texture
= Tex
407 TexSlot
.use_map_alpha
= True
408 TexSlot
.use_map_color_diffuse
= False
409 TexSlot
.texture_coords
= 'UV'
410 if OpacityMode
== 'TEX': TexSlot
.alpha_factor
= LayerOpacity
411 elif OpacityMode
== 'MAT': TexSlot
.blend_type
= 'MULTIPLY'
413 if not Active
.material_slots
:
414 bpy
.ops
.object.material_slot_add()
416 Active
.material_slots
[0].material
= Mat
426 Make3DLayer(Layer
['LayerName'].replace(' ', '_'),
427 Layer
['LayerNameShort'].replace(' ', '_'),
429 Layer
['LayerCoords'],
430 Layer
['RenderLayer'],
432 Layer
['LayerOpacity'],
439 #-------------------------------------------------
442 Scene
.use_nodes
= True
444 Tree
= Scene
.node_tree
452 LayerLen
= len(LayerList
)
454 for Layer
in LayerList
:
458 X_Offset
= (500*Offset
)
459 Y_Offset
= (-300*Offset
)
461 Node
= Tree
.nodes
.new('CompositorNodeRLayers')
462 Node
.location
= (-500+X_Offset
, 300+Y_Offset
)
463 Node
.name
= 'R_'+ str(Offset
)
465 Node
.layer
= Layer
[0]
468 Node_V
= Tree
.nodes
.new('CompositorNodeViewer')
469 Node_V
.name
= Layer
[0]
470 Node_V
.location
= (-200+X_Offset
, 200+Y_Offset
)
472 Tree
.links
.new(Node
.outputs
[0], Node_V
.inputs
[0])
474 if LayerLen
> Offset
:
476 Mode
= LayerList
[Offset
][1] # has to go one step further
477 LayerOpacity
= LayerList
[Offset
][2]
479 if not Mode
in {'Normal', '-1'}:
481 Node
= Tree
.nodes
.new('CompositorNodeMixRGB')
482 if OpacityMode
== 'COMPO': Node
.inputs
['Fac'].default_value
= LayerOpacity
483 else: Node
.inputs
['Fac'].default_value
= 1
484 Node
.use_alpha
= True
486 if Mode
in {'Addition', '7'}: Node
.blend_type
= 'ADD'
487 elif Mode
in {'Subtract', '8'}: Node
.blend_type
= 'SUBTRACT'
488 elif Mode
in {'Multiply', '3'}: Node
.blend_type
= 'MULTIPLY'
489 elif Mode
in {'DarkenOnly', '9'}: Node
.blend_type
= 'DARKEN'
490 elif Mode
in {'Dodge', '16'}: Node
.blend_type
= 'DODGE'
491 elif Mode
in {'LightenOnly', '10'}: Node
.blend_type
= 'LIGHTEN'
492 elif Mode
in {'Difference', '6'}: Node
.blend_type
= 'DIFFERENCE'
493 elif Mode
in {'Divide', '15'}: Node
.blend_type
= 'DIVIDE'
494 elif Mode
in {'Overlay', '5'}: Node
.blend_type
= 'OVERLAY'
495 elif Mode
in {'Screen', '4'}: Node
.blend_type
= 'SCREEN'
496 elif Mode
in {'Burn', '17'}: Node
.blend_type
= 'BURN'
497 elif Mode
in {'Color', '13'}: Node
.blend_type
= 'COLOR'
498 elif Mode
in {'Value', '14'}: Node
.blend_type
= 'VALUE'
499 elif Mode
in {'Saturation', '12'}: Node
.blend_type
= 'SATURATION'
500 elif Mode
in {'Hue', '11'}: Node
.blend_type
= 'HUE'
501 elif Mode
in {'Softlight', '19'}: Node
.blend_type
= 'SOFT_LIGHT'
505 Node
= Tree
.nodes
.new('CompositorNodeAlphaOver')
506 if OpacityMode
== 'COMPO': Node
.inputs
['Fac'].default_value
= LayerOpacity
507 Node
.name
= 'M_' + str(Offset
)
508 Node
.location
= (300+X_Offset
, 250+Y_Offset
)
511 Node_V
= Tree
.nodes
.new('CompositorNodeViewer')
512 Node_V
.name
= Layer
[0]
513 Node_V
.location
= (500+X_Offset
, 350+Y_Offset
)
515 Tree
.links
.new(Node
.outputs
[0], Node_V
.inputs
[0])
518 Node
= Tree
.nodes
.new('CompositorNodeComposite')
519 Node
.name
= 'Composite'
520 Node
.location
= (400+X_Offset
, 350+Y_Offset
)
522 Nodes
= bpy
.context
.scene
.node_tree
.nodes
525 for i
in range (1, LayerLen
+ 1):
527 Tree
.links
.new(Nodes
['R_'+str(i
)].outputs
[0], Nodes
['M_'+str(i
)].inputs
[1])
529 Tree
.links
.new(Nodes
['M_'+str(i
-1)].outputs
[0], Nodes
['M_'+str(i
)].inputs
[1])
530 if 1 < i
< LayerLen
+1:
531 Tree
.links
.new(Nodes
['R_'+str(i
)].outputs
[0], Nodes
['M_'+str(i
-1)].inputs
[2])
533 Tree
.links
.new(Nodes
['M_'+str(i
-1)].outputs
[0], Nodes
['Composite'].inputs
[0])
535 Tree
.links
.new(Nodes
['R_1'].outputs
[0], Nodes
['Composite'].inputs
[0])
538 i
.location
[0] += -250*Offset
539 i
.location
[1] += 150*Offset
543 #------------------------------------------------------------------------
544 import os
, subprocess
546 from bpy
.props
import *
550 class GIMPImageToScene(bpy
.types
.Operator
):
552 bl_idname
= "import.gimp_image_to_scene"
553 bl_label
= "GIMP Image to Scene"
554 bl_description
= "Imports GIMP multilayer image files into 3D Scenes"
555 bl_options
= {'REGISTER', 'UNDO'}
557 filename
= StringProperty(name
="File Name",
558 description
="Name of the file")
559 directory
= StringProperty(name
="Directory",
560 description
="Directory of the file")
562 LayerViewers
= BoolProperty(name
="Layer Viewers",
563 description
="Add Viewer nodes to each Render Layer node",
566 MixerViewers
= BoolProperty(name
="Mixer Viewers",
567 description
="Add Viewer nodes to each Mix node",
570 AlphaMode
= EnumProperty(name
="Alpha Mode",
571 description
="Representation of alpha information in the RGBA pixels",
573 ('STRAIGHT', 'Texture Alpha Factor', 'Transparent RGB and alpha pixels are unmodified'),
574 ('PREMUL', 'Material Alpha Value', 'Transparent RGB pixels are multiplied by the alpha channel')),
577 ShadelessMats
= BoolProperty(name
="Shadeless Material",
578 description
="Set Materials as Shadeless",
581 OpacityMode
= EnumProperty(name
="Opacity Mode",
582 description
="Layer Opacity management",
584 ('TEX', 'Texture Alpha Factor', ''),
585 ('MAT', 'Material Alpha Value', ''),
586 ('COMPO', 'Mixer Node Factor', ''),
587 ('BAKE', 'Baked in Image Alpha', '')),
590 SetCamera
= BoolProperty(name
="Set Camera",
591 description
="Create an Ortho Camera matching image resolution",
594 SetupCompo
= BoolProperty(name
="Setup Node Compositing",
595 description
="Create a compositing node setup (will delete existing nodes)",
598 GroupUntagged
= BoolProperty(name
="Group Untagged",
599 description
="Layers with no tag go to a single Render Layer",
602 LayerOffset
= FloatProperty(name
="Layer Separation",
603 description
="Distance between each 3D Layer in the Z axis",
607 LayerScale
= FloatProperty(name
="Layer Scale",
608 description
="Scale pixel resolution by Blender units",
612 def draw(self
, context
):
616 box
.label('3D Layers:', icon
='SORTSIZE')
617 box
.prop(self
, 'SetCamera', icon
='OUTLINER_DATA_CAMERA')
618 box
.prop(self
, 'OpacityMode', icon
='GHOST')
619 if self
.OpacityMode
== 'COMPO' and self
.SetupCompo
== False:
620 box
.label('Tip: Enable Node Compositing', icon
='INFO')
621 box
.prop(self
, 'AlphaMode', icon
='IMAGE_RGB_ALPHA')
622 box
.prop(self
, 'ShadelessMats', icon
='SOLID')
623 box
.prop(self
, 'LayerOffset')
624 box
.prop(self
, 'LayerScale')
627 box
.label('Compositing:', icon
='RENDERLAYERS')
628 box
.prop(self
, 'SetupCompo', icon
='NODETREE')
630 box
.prop(self
, 'GroupUntagged', icon
='IMAGE_ZDEPTH')
631 box
.prop(self
, 'LayerViewers', icon
='NODE')
632 box
.prop(self
, 'MixerViewers', icon
='NODE')
634 def execute(self
, context
):
636 filename
= self
.filename
637 directory
= self
.directory
640 LayerViewers
= self
.LayerViewers
641 MixerViewers
= self
.MixerViewers
642 OpacityMode
= self
.OpacityMode
643 AlphaMode
= self
.AlphaMode
644 ShadelessMats
= self
.ShadelessMats
645 SetCamera
= self
.SetCamera
646 SetupCompo
= self
.SetupCompo
647 GroupUntagged
= self
.GroupUntagged
648 LayerOffset
= self
.LayerOffset
649 LayerScale
= self
.LayerScale
652 if filename
.endswith('.xcf'): Ext
= '.xcf'
653 elif filename
.endswith('.xjt'): Ext
= '.xjt'
657 ret
= main(self
.report
, filename
, directory
, LayerViewers
, MixerViewers
, LayerOffset
,
658 LayerScale
, OpacityMode
, AlphaMode
, ShadelessMats
,
659 SetCamera
, SetupCompo
, GroupUntagged
, Ext
)
663 self
.report({'ERROR'},"Selected file wasn't valid, try .xcf or .xjt")
668 def invoke(self
, context
, event
):
669 wm
= bpy
.context
.window_manager
670 wm
.fileselect_add(self
)
672 return {'RUNNING_MODAL'}
675 # Registering / Unregister
676 def menu_func(self
, context
):
677 self
.layout
.operator(GIMPImageToScene
.bl_idname
, text
="GIMP Image to Scene (.xcf, .xjt)", icon
='PLUGIN')
681 bpy
.utils
.register_module(__name__
)
683 bpy
.types
.INFO_MT_file_import
.append(menu_func
)
687 bpy
.utils
.unregister_module(__name__
)
689 bpy
.types
.INFO_MT_file_import
.remove(menu_func
)
692 if __name__
== "__main__":