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, 57, 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 "tracker_url": "http://projects.blender.org/tracker/index.php?"
30 "func=detail&aid=25136",
31 "category": "Import-Export"}
34 This script imports GIMP layered image files into 3D Scenes (.xcf, .xjt)
37 def main(File
, Path
, LayerViewers
, MixerViewers
, LayerOffset
,
38 LayerScale
, OpacityMode
, AlphaMode
, ShadelessMats
,
39 SetCamera
, SetupCompo
, GroupUntagged
, Ext
):
41 #-------------------------------------------------
43 #Folder = '['+File.rstrip(Ext)+']'+'_images/'
44 Folder
= 'images_'+'['+File
.rstrip(Ext
)+']/'
46 if not bpy
.data
.is_saved
:
47 PathSaveRaw
= Path
+Folder
48 PathSave
= PathSaveRaw
.replace(' ', '\ ')
49 try: os
.mkdir(PathSaveRaw
)
52 PathSave
= bpy
.data
.filepath
53 RSlash
= PathSave
.rfind('/')
54 PathSaveRaw
= PathSave
[:RSlash
+1]+Folder
55 PathSave
= PathSaveRaw
.replace(' ', '\ ')
56 try: os
.mkdir(PathSaveRaw
)
58 PathSaveRaw
= bpy
.path
.relpath(PathSaveRaw
)+'/'
61 Path
= Path
.replace(' ', '\ ')
64 #-------------------------------------------------
68 IMG
= tarfile
.open ('%s%s' % (PathRaw
, File
))
69 PRP
= IMG
.extractfile('PRP')
71 Members
= IMG
.getmembers()
73 for Member
in Members
:
75 if Name
.startswith('l') and Name
.endswith('.jpg'):
76 IMG
.extract(Name
, path
=PathSaveRaw
)
78 #-------------------------------------------------
81 for Line
in PRP
.readlines():
84 if Line
.startswith("b'GIMP_XJ_IMAGE"):
85 for Segment
in Line
.split():
86 if Segment
.startswith('w/h:'):
87 ResX
, ResY
= map (int, Segment
[4:].split(','))
88 if Line
.startswith(("b'L", "b'l")):
90 """The "nice" method to check if layer has alpha channel
91 sadly GIMP sometimes decides not to export an alpha channel
92 if it's pure white so we are not completly sure here yet"""
93 if Line
.startswith("b'L"): HasAlpha
= True
94 else: HasAlpha
= False
100 for Segment
in Line
.split():
102 if Segment
.startswith("b'"):
103 imageFile
= 'l' + Segment
[3:] + '.jpg'
104 imageFileAlpha
='la'+Segment
[3:]+'.jpg'
106 """Phisically double checking if alpha image exists
107 now we can be sure! (damn GIMP)"""
109 if not os
.path
.isfile(PathSaveRaw
+imageFileAlpha
): HasAlpha
= False
111 # Get Widht and Height from images
112 data
= open(PathSaveRaw
+imageFile
, "rb").read()
119 for k
in range(len(hexList
)-1):
120 if hexList
[k
] == 'FF' and (hexList
[k
+1] == 'C0' or hexList
[k
+1] == 'C2'):
121 ow
= int(hexList
[k
+7],16)*256 + int(hexList
[k
+8],16)
122 oh
= int(hexList
[k
+5],16)*256 + int(hexList
[k
+6],16)
124 elif Segment
.startswith('md:'): # mode
127 elif Segment
.startswith('op:'): # opacity
128 op
= float(Segment
[3:])*.01
130 elif Segment
.startswith('o:'): # origin
131 ox
, oy
= map(int, Segment
[2:].split(','))
133 elif Segment
.startswith('n:'): # name
135 OpenBracket
= n
.find ('[')
136 CloseBracket
= n
.find (']')
138 if OpenBracket
!= -1 and CloseBracket
!= -1:
139 RenderLayer
= n
[OpenBracket
+1:CloseBracket
]
140 NameShort
= n
[:OpenBracket
]
146 os
.rename(PathSaveRaw
+imageFile
, PathSaveRaw
+NameShort
+'.jpg')
147 if HasAlpha
: os
.rename(PathSaveRaw
+imageFileAlpha
, PathSaveRaw
+NameShort
+'_A'+'.jpg')
149 IMGs
.append({'LayerMode':md
, 'LayerOpacity':op
,
150 'LayerName':n
, 'LayerNameShort':NameShort
,
151 'RenderLayer':RenderLayer
, 'LayerCoords':[ow
, oh
, ox
, oy
], 'HasAlpha':HasAlpha
})
153 else: # Ext == '.xcf':
155 #-------------------------------------------------
159 #-------------------------------------------------
162 CMD
= '%s %s%s' % (XCFInfo
, Path
, File
)
167 for Line
in Info
.readlines():
168 if Line
.startswith ('+'):
170 Line
= Line
.split(' ', 4)
172 RenderLayer
= Line
[4]
174 OpenBracket
= RenderLayer
.find ('[')
175 CloseBracket
= RenderLayer
.find (']')
177 if OpenBracket
!= -1 and CloseBracket
!= -1:
178 RenderLayer
= RenderLayer
[OpenBracket
+1:CloseBracket
]
179 NameShort
= Line
[4][:OpenBracket
]
181 NameShort
= Line
[4].rstrip()
183 RenderLayer
= '__Undefined__'
185 RenderLayer
= NameShort
188 Slash
= LineThree
.find('/')
193 Mode
= LineThree
[:Slash
]
194 Opacity
= float(LineThree
[Slash
+1:LineThree
.find('%')])*.01
198 'LayerOpacity': Opacity
,
199 'LayerName': Line
[4].rstrip(),
200 'LayerNameShort': NameShort
,
201 'LayerCoords': list(map(int, Line
[1].replace('x', ' ').replace('+', ' +').replace('-', ' -').split())),
202 'RenderLayer': RenderLayer
,
205 elif Line
.startswith('Version'):
206 ResX
, ResY
= map (int, Line
.split()[2].split('x'))
208 #-------------------------------------------------
210 if OpacityMode
== 'BAKE':
213 Opacity
= ' --percent 100'
215 CMD
= ('%s -C %s%s -o %s%s.png "%s"%s' %
216 (XCF2PNG
, Path
, File
, PathSave
, Layer
['LayerName'].replace(' ', '_'), Layer
['LayerName'], Opacity
))
219 #-------------------------------------------------
220 Scene
= bpy
.context
.scene
221 #-------------------------------------------------
225 bpy
.ops
.object.camera_add(location
=(0, 0, 10))
227 Camera
= bpy
.context
.active_object
.data
229 Camera
.type = 'ORTHO'
230 Camera
.ortho_scale
= ResX
* .01
232 #-------------------------------------------------
235 Render
= Scene
.render
238 Render
.resolution_x
= ResX
239 Render
.resolution_y
= ResY
240 Render
.resolution_percentage
= 100
241 Render
.alpha_mode
= 'TRANSPARENT'
243 #-------------------------------------------------
246 Scene
.game_settings
.material_mode
= 'GLSL'
248 Areas
= bpy
.context
.screen
.areas
251 if Area
.type == 'VIEW_3D':
252 Area
.spaces
.active
.viewport_shade
= 'TEXTURED'
253 Area
.spaces
.active
.show_textured_solid
= True
254 Area
.spaces
.active
.show_floor
= False
256 #-------------------------------------------------
259 def Make3DLayer (Name
, NameShort
, Z
, Coords
, RenderLayer
, LayerMode
, LayerOpacity
, HasAlpha
):
264 if not bpy
.context
.scene
.render
.layers
.get(RenderLayer
):
266 bpy
.ops
.scene
.render_layer_add()
268 LayerActive
= bpy
.context
.scene
.render
.layers
.active
269 LayerActive
.name
= RenderLayer
270 LayerActive
.use_pass_vector
= True
271 LayerActive
.use_sky
= False
272 LayerActive
.use_edge_enhance
= False
273 LayerActive
.use_strand
= False
274 LayerActive
.use_halo
= False
277 for i
in range (0,20):
278 if not i
== LayerNum
:
279 LayerActive
.layers
[i
] = False
281 bpy
.context
.scene
.layers
[LayerNum
] = True
283 LayerFlags
[RenderLayer
] = bpy
.context
.scene
.render
.layers
.active
.layers
285 LayerList
.append([RenderLayer
, LayerMode
, LayerOpacity
])
290 bpy
.ops
.mesh
.primitive_plane_add(view_align
=False,
291 enter_editmode
=False,
294 bpy
.ops
.object.transform_apply(location
=False, rotation
=True, scale
=False)
297 Active
= bpy
.context
.active_object
300 Active
.layers
= LayerFlags
[RenderLayer
]
303 (float(Coords
[2])-(ResX
*0.5))*LayerScale
,
304 (-float(Coords
[3])+(ResY
*0.5))*LayerScale
, Z
)
306 for Vert
in Active
.data
.vertices
:
310 Active
.dimensions
= float(Coords
[0])*LayerScale
, float(Coords
[1])*LayerScale
, 0
312 bpy
.ops
.object.transform_apply(location
=False, rotation
=False, scale
=True)
314 bpy
.ops
.object.origin_set(type='ORIGIN_GEOMETRY', center
='MEDIAN')
316 Active
.show_wire
= True
318 Active
.name
= NameShort
319 bpy
.ops
.mesh
.uv_texture_add()
323 '''if bpy.data.materials.get(NameShort):
324 Mat = bpy.data.materials[NameShort]
325 if not Active.material_slots:
326 bpy.ops.object.material_slot_add()
327 Active.material_slots[0].material = Mat
330 Mat
= bpy
.data
.materials
.new(NameShort
)
331 Mat
.diffuse_color
= (1,1,1)
332 Mat
.use_raytrace
= False
333 Mat
.use_shadows
= False
334 Mat
.use_cast_buffer_shadows
= False
335 Mat
.use_cast_approximate
= False
337 Mat
.use_transparency
= True
338 if OpacityMode
== 'MAT': Mat
.alpha
= LayerOpacity
340 if ShadelessMats
: Mat
.use_shadeless
= True
344 Tex
= bpy
.data
.textures
.new(NameShort
, 'IMAGE')
345 Tex
.extension
= 'CLIP'
346 Tex
.use_preview_alpha
= True
348 Img
= bpy
.data
.images
.new(NameShort
, 128, 128)
350 Img
.alpha_mode
= AlphaMode
351 Img
.filepath
= '%s%s%s' % (PathSaveRaw
, Name
, ExtSave
)
353 UVFace
= Active
.data
.uv_textures
[0].data
[0]
358 Mat
.texture_slots
.add()
359 TexSlot
= Mat
.texture_slots
[0]
360 TexSlot
.texture
= Tex
361 TexSlot
.use_map_alpha
= True
362 TexSlot
.texture_coords
= 'UV'
363 if OpacityMode
== 'TEX': TexSlot
.alpha_factor
= LayerOpacity
364 elif OpacityMode
== 'MAT': TexSlot
.blend_type
= 'MULTIPLY'
366 else: # Ext == '.xjt'
368 Tex
= bpy
.data
.textures
.new(NameShort
, 'IMAGE')
369 Tex
.extension
= 'CLIP'
371 Img
= bpy
.data
.images
.new(NameShort
, 128, 128)
373 Img
.filepath
= '%s%s%s' % (PathSaveRaw
, Name
, ExtSave
)
375 UVFace
= Active
.data
.uv_textures
[0].data
[0]
380 Mat
.texture_slots
.add()
381 TexSlot
= Mat
.texture_slots
[0]
382 TexSlot
.texture
= Tex
383 TexSlot
.texture_coords
= 'UV'
387 Tex
= bpy
.data
.textures
.new(NameShort
+'_A', 'IMAGE')
388 Tex
.extension
= 'CLIP'
389 Tex
.use_preview_alpha
= True
391 Img
= bpy
.data
.images
.new(NameShort
+'_A', 128, 128)
393 Img
.alpha_mode
= AlphaMode
394 Img
.filepath
= '%s%s_A%s' % (PathSaveRaw
, Name
, ExtSave
)
395 Img
.use_alpha
= False
399 Mat
.texture_slots
.add()
400 TexSlot
= Mat
.texture_slots
[1]
401 TexSlot
.texture
= Tex
402 TexSlot
.use_map_alpha
= True
403 TexSlot
.use_map_color_diffuse
= False
404 TexSlot
.texture_coords
= 'UV'
405 if OpacityMode
== 'TEX': TexSlot
.alpha_factor
= LayerOpacity
406 elif OpacityMode
== 'MAT': TexSlot
.blend_type
= 'MULTIPLY'
408 if not Active
.material_slots
:
409 bpy
.ops
.object.material_slot_add()
411 Active
.material_slots
[0].material
= Mat
421 Make3DLayer(Layer
['LayerName'].replace(' ', '_'),
422 Layer
['LayerNameShort'].replace(' ', '_'),
424 Layer
['LayerCoords'],
425 Layer
['RenderLayer'],
427 Layer
['LayerOpacity'],
434 #-------------------------------------------------
437 Scene
.use_nodes
= True
439 Tree
= Scene
.node_tree
447 LayerLen
= len(LayerList
)
449 for Layer
in LayerList
:
453 X_Offset
= (500*Offset
)
454 Y_Offset
= (-300*Offset
)
456 Node
= Tree
.nodes
.new('R_LAYERS')
457 Node
.location
= (-500+X_Offset
, 300+Y_Offset
)
458 Node
.name
= 'R_'+ str(Offset
)
460 Node
.layer
= Layer
[0]
463 Node_V
= Tree
.nodes
.new('VIEWER')
464 Node_V
.name
= Layer
[0]
465 Node_V
.location
= (-200+X_Offset
, 200+Y_Offset
)
467 Tree
.links
.new(Node
.outputs
[0], Node_V
.inputs
[0])
469 if LayerLen
> Offset
:
471 Mode
= LayerList
[Offset
][1] # has to go one step further
472 LayerOpacity
= LayerList
[Offset
][2]
474 if not Mode
in {'Normal', '-1'}:
476 Node
= Tree
.nodes
.new('MIX_RGB')
477 if OpacityMode
== 'COMPO': Node
.inputs
['Fac'].default_value
[0] = LayerOpacity
478 else: Node
.inputs
['Fac'].default_value
[0] = 1
479 Node
.use_alpha
= True
481 if Mode
in {'Addition', '7'}: Node
.blend_type
= 'ADD'
482 elif Mode
in {'Subtract', '8'}: Node
.blend_type
= 'SUBTRACT'
483 elif Mode
in {'Multiply', '3'}: Node
.blend_type
= 'MULTIPLY'
484 elif Mode
in {'DarkenOnly', '9'}: Node
.blend_type
= 'DARKEN'
485 elif Mode
in {'Dodge', '16'}: Node
.blend_type
= 'DODGE'
486 elif Mode
in {'LightenOnly', '10'}: Node
.blend_type
= 'LIGHTEN'
487 elif Mode
in {'Difference', '6'}: Node
.blend_type
= 'DIFFERENCE'
488 elif Mode
in {'Divide', '15'}: Node
.blend_type
= 'DIVIDE'
489 elif Mode
in {'Overlay', '5'}: Node
.blend_type
= 'OVERLAY'
490 elif Mode
in {'Screen', '4'}: Node
.blend_type
= 'SCREEN'
491 elif Mode
in {'Burn', '17'}: Node
.blend_type
= 'BURN'
492 elif Mode
in {'Color', '13'}: Node
.blend_type
= 'COLOR'
493 elif Mode
in {'Value', '14'}: Node
.blend_type
= 'VALUE'
494 elif Mode
in {'Saturation', '12'}: Node
.blend_type
= 'SATURATION'
495 elif Mode
in {'Hue', '11'}: Node
.blend_type
= 'HUE'
496 elif Mode
in {'Softlight', '19'}: Node
.blend_type
= 'SOFT_LIGHT'
500 Node
= Tree
.nodes
.new('ALPHAOVER')
501 if OpacityMode
== 'COMPO': Node
.inputs
['Fac'].default_value
[0] = LayerOpacity
502 Node
.name
= 'M_' + str(Offset
)
503 Node
.location
= (300+X_Offset
, 250+Y_Offset
)
506 Node_V
= Tree
.nodes
.new('VIEWER')
507 Node_V
.name
= Layer
[0]
508 Node_V
.location
= (500+X_Offset
, 350+Y_Offset
)
510 Tree
.links
.new(Node
.outputs
[0], Node_V
.inputs
[0])
513 Node
= Tree
.nodes
.new('COMPOSITE')
514 Node
.name
= 'Composite'
515 Node
.location
= (400+X_Offset
, 350+Y_Offset
)
517 Nodes
= bpy
.context
.scene
.node_tree
.nodes
520 for i
in range (1, LayerLen
+ 1):
522 Tree
.links
.new(Nodes
['R_'+str(i
)].outputs
[0], Nodes
['M_'+str(i
)].inputs
[1])
524 Tree
.links
.new(Nodes
['M_'+str(i
-1)].outputs
[0], Nodes
['M_'+str(i
)].inputs
[1])
525 if 1 < i
< LayerLen
+1:
526 Tree
.links
.new(Nodes
['R_'+str(i
)].outputs
[0], Nodes
['M_'+str(i
-1)].inputs
[2])
528 Tree
.links
.new(Nodes
['M_'+str(i
-1)].outputs
[0], Nodes
['Composite'].inputs
[0])
530 Tree
.links
.new(Nodes
['R_1'].outputs
[0], Nodes
['Composite'].inputs
[0])
533 i
.location
[0] += -250*Offset
534 i
.location
[1] += 150*Offset
536 #------------------------------------------------------------------------
539 from bpy
.props
import *
543 class GIMPImageToScene(bpy
.types
.Operator
):
545 bl_idname
= "import.gimp_image_to_scene"
546 bl_label
= "GIMP Image to Scene"
547 bl_description
= "Imports GIMP multilayer image files into 3D Scenes"
548 bl_options
= {'REGISTER', 'UNDO'}
550 filename
= StringProperty(name
="File Name",
551 description
="Name of the file")
552 directory
= StringProperty(name
="Directory",
553 description
="Directory of the file")
555 LayerViewers
= BoolProperty(name
="Layer Viewers",
556 description
="Add Viewer nodes to each Render Layer node",
559 MixerViewers
= BoolProperty(name
="Mixer Viewers",
560 description
="Add Viewer nodes to each Mix node",
563 AlphaMode
= EnumProperty(name
="Alpha Mode",
564 description
="Representation of alpha information in the RGBA pixels",
566 ('STRAIGHT', 'Texture Alpha Factor', 'Transparent RGB and alpha pixels are unmodified'),
567 ('PREMUL', 'Material Alpha Value', 'Transparent RGB pixels are multiplied by the alpha channel')),
570 ShadelessMats
= BoolProperty(name
="Shadeless Material",
571 description
="Set Materials as Shadeless",
574 OpacityMode
= EnumProperty(name
="Opacity Mode",
575 description
="Layer Opacity management",
577 ('TEX', 'Texture Alpha Factor', ''),
578 ('MAT', 'Material Alpha Value', ''),
579 ('COMPO', 'Mixer Node Factor', ''),
580 ('BAKE', 'Baked in Image Alpha', '')),
583 SetCamera
= BoolProperty(name
="Set Camera",
584 description
="Create an Ortho Camera matching image resolution",
587 SetupCompo
= BoolProperty(name
="Setup Node Compositing",
588 description
="Create a compositing node setup (will delete existing nodes)",
591 GroupUntagged
= BoolProperty(name
="Group Untagged",
592 description
="Layers with no tag go to a single Render Layer",
595 LayerOffset
= FloatProperty(name
="Layer Separation",
596 description
="Distance between each 3D Layer in the Z axis",
600 LayerScale
= FloatProperty(name
="Layer Scale",
601 description
="Scale pixel resolution by Blender units",
605 def draw(self
, context
):
609 box
.label('3D Layers:', icon
='SORTSIZE')
610 box
.prop(self
, 'SetCamera', icon
='OUTLINER_DATA_CAMERA')
611 box
.prop(self
, 'OpacityMode', icon
='GHOST')
612 if self
.OpacityMode
== 'COMPO' and self
.SetupCompo
== False:
613 box
.label('Tip: Enable Node Compositing', icon
='INFO')
614 box
.prop(self
, 'AlphaMode', icon
='IMAGE_RGB_ALPHA')
615 box
.prop(self
, 'ShadelessMats', icon
='SOLID')
616 box
.prop(self
, 'LayerOffset')
617 box
.prop(self
, 'LayerScale')
620 box
.label('Compositing:', icon
='RENDERLAYERS')
621 box
.prop(self
, 'SetupCompo', icon
='NODETREE')
623 box
.prop(self
, 'GroupUntagged', icon
='IMAGE_ZDEPTH')
624 box
.prop(self
, 'LayerViewers', icon
='NODE')
625 box
.prop(self
, 'MixerViewers', icon
='NODE')
627 def execute(self
, context
):
629 filename
= self
.filename
630 directory
= self
.directory
633 LayerViewers
= self
.LayerViewers
634 MixerViewers
= self
.MixerViewers
635 OpacityMode
= self
.OpacityMode
636 AlphaMode
= self
.AlphaMode
637 ShadelessMats
= self
.ShadelessMats
638 SetCamera
= self
.SetCamera
639 SetupCompo
= self
.SetupCompo
640 GroupUntagged
= self
.GroupUntagged
641 LayerOffset
= self
.LayerOffset
642 LayerScale
= self
.LayerScale
645 if filename
.endswith('.xcf'): Ext
= '.xcf'
646 elif filename
.endswith('.xjt'): Ext
= '.xjt'
650 main(filename
, directory
, LayerViewers
, MixerViewers
, LayerOffset
,
651 LayerScale
, OpacityMode
, AlphaMode
, ShadelessMats
,
652 SetCamera
, SetupCompo
, GroupUntagged
, Ext
)
654 self
.report({'ERROR'},"Selected file wasn't valid, try .xcf or .xjt")
658 def invoke(self
, context
, event
):
659 wm
= bpy
.context
.window_manager
660 wm
.fileselect_add(self
)
662 return {'RUNNING_MODAL'}
665 # Registering / Unregister
666 def menu_func(self
, context
):
667 self
.layout
.operator(GIMPImageToScene
.bl_idname
, text
="GIMP Image to Scene (.xcf, .xjt)", icon
='PLUGIN')
671 bpy
.utils
.register_module(__name__
)
673 bpy
.types
.INFO_MT_file_import
.append(menu_func
)
677 bpy
.utils
.unregister_module(__name__
)
679 bpy
.types
.INFO_MT_file_import
.remove(menu_func
)
682 if __name__
== "__main__":