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": "https://developer.blender.org/T25136",
30 "category": "Import-Export"}
33 This script imports GIMP layered image files into 3D Scenes (.xcf, .xjt)
36 def main(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 #-------------------------------------------------
161 CMD
= '%s %s%s' % (XCFInfo
, Path
, File
)
166 for Line
in Info
.readlines():
167 if Line
.startswith ('+'):
169 Line
= Line
.split(' ', 4)
171 RenderLayer
= Line
[4]
173 OpenBracket
= RenderLayer
.find ('[')
174 CloseBracket
= RenderLayer
.find (']')
176 if OpenBracket
!= -1 and CloseBracket
!= -1:
177 RenderLayer
= RenderLayer
[OpenBracket
+1:CloseBracket
]
178 NameShort
= Line
[4][:OpenBracket
]
180 NameShort
= Line
[4].rstrip()
182 RenderLayer
= '__Undefined__'
184 RenderLayer
= NameShort
187 Slash
= LineThree
.find('/')
192 Mode
= LineThree
[:Slash
]
193 Opacity
= float(LineThree
[Slash
+1:LineThree
.find('%')])*.01
197 'LayerOpacity': Opacity
,
198 'LayerName': Line
[4].rstrip(),
199 'LayerNameShort': NameShort
,
200 'LayerCoords': list(map(int, Line
[1].replace('x', ' ').replace('+', ' +').replace('-', ' -').split())),
201 'RenderLayer': RenderLayer
,
204 elif Line
.startswith('Version'):
205 ResX
, ResY
= map (int, Line
.split()[2].split('x'))
207 #-------------------------------------------------
209 if OpacityMode
== 'BAKE':
212 Opacity
= ' --percent 100'
214 CMD
= ('%s -C %s%s -o %s%s.png "%s"%s' %
215 (XCF2PNG
, Path
, File
, PathSave
, Layer
['LayerName'].replace(' ', '_'), Layer
['LayerName'], Opacity
))
218 #-------------------------------------------------
219 Scene
= bpy
.context
.scene
220 #-------------------------------------------------
224 bpy
.ops
.object.camera_add(location
=(0, 0, 10))
226 Camera
= bpy
.context
.active_object
.data
228 Camera
.type = 'ORTHO'
229 Camera
.ortho_scale
= ResX
* .01
231 #-------------------------------------------------
234 Render
= Scene
.render
237 Render
.resolution_x
= ResX
238 Render
.resolution_y
= ResY
239 Render
.resolution_percentage
= 100
240 Render
.alpha_mode
= 'TRANSPARENT'
242 #-------------------------------------------------
245 Scene
.game_settings
.material_mode
= 'GLSL'
247 Areas
= bpy
.context
.screen
.areas
250 if Area
.type == 'VIEW_3D':
251 Area
.spaces
.active
.viewport_shade
= 'TEXTURED'
252 Area
.spaces
.active
.show_textured_solid
= True
253 Area
.spaces
.active
.show_floor
= False
255 #-------------------------------------------------
258 def Make3DLayer (Name
, NameShort
, Z
, Coords
, RenderLayer
, LayerMode
, LayerOpacity
, HasAlpha
):
263 if not bpy
.context
.scene
.render
.layers
.get(RenderLayer
):
265 bpy
.ops
.scene
.render_layer_add()
267 LayerActive
= bpy
.context
.scene
.render
.layers
.active
268 LayerActive
.name
= RenderLayer
269 LayerActive
.use_pass_vector
= True
270 LayerActive
.use_sky
= False
271 LayerActive
.use_edge_enhance
= False
272 LayerActive
.use_strand
= False
273 LayerActive
.use_halo
= False
276 for i
in range (0,20):
277 if not i
== LayerNum
:
278 LayerActive
.layers
[i
] = False
280 bpy
.context
.scene
.layers
[LayerNum
] = True
282 LayerFlags
[RenderLayer
] = bpy
.context
.scene
.render
.layers
.active
.layers
284 LayerList
.append([RenderLayer
, LayerMode
, LayerOpacity
])
289 bpy
.ops
.mesh
.primitive_plane_add(view_align
=False,
290 enter_editmode
=False,
293 bpy
.ops
.object.transform_apply(location
=False, rotation
=True, scale
=False)
296 Active
= bpy
.context
.active_object
299 Active
.layers
= LayerFlags
[RenderLayer
]
302 (float(Coords
[2])-(ResX
*0.5))*LayerScale
,
303 (-float(Coords
[3])+(ResY
*0.5))*LayerScale
, Z
)
305 for Vert
in Active
.data
.vertices
:
309 Active
.dimensions
= float(Coords
[0])*LayerScale
, float(Coords
[1])*LayerScale
, 0
311 bpy
.ops
.object.transform_apply(location
=False, rotation
=False, scale
=True)
313 bpy
.ops
.object.origin_set(type='ORIGIN_GEOMETRY', center
='MEDIAN')
315 Active
.show_wire
= True
317 Active
.name
= NameShort
318 bpy
.ops
.mesh
.uv_texture_add()
322 '''if bpy.data.materials.get(NameShort):
323 Mat = bpy.data.materials[NameShort]
324 if not Active.material_slots:
325 bpy.ops.object.material_slot_add()
326 Active.material_slots[0].material = Mat
329 Mat
= bpy
.data
.materials
.new(NameShort
)
330 Mat
.diffuse_color
= (1,1,1)
331 Mat
.use_raytrace
= False
332 Mat
.use_shadows
= False
333 Mat
.use_cast_buffer_shadows
= False
334 Mat
.use_cast_approximate
= False
336 Mat
.use_transparency
= True
337 if OpacityMode
== 'MAT': Mat
.alpha
= LayerOpacity
339 if ShadelessMats
: Mat
.use_shadeless
= True
343 Tex
= bpy
.data
.textures
.new(NameShort
, 'IMAGE')
344 Tex
.extension
= 'CLIP'
345 Tex
.use_preview_alpha
= True
347 Img
= bpy
.data
.images
.new(NameShort
, 128, 128)
349 Img
.alpha_mode
= AlphaMode
350 Img
.filepath
= '%s%s%s' % (PathSaveRaw
, Name
, ExtSave
)
352 UVFace
= Active
.data
.uv_textures
[0].data
[0]
357 Mat
.texture_slots
.add()
358 TexSlot
= Mat
.texture_slots
[0]
359 TexSlot
.texture
= Tex
360 TexSlot
.use_map_alpha
= True
361 TexSlot
.texture_coords
= 'UV'
362 if OpacityMode
== 'TEX': TexSlot
.alpha_factor
= LayerOpacity
363 elif OpacityMode
== 'MAT': TexSlot
.blend_type
= 'MULTIPLY'
365 else: # Ext == '.xjt'
367 Tex
= bpy
.data
.textures
.new(NameShort
, 'IMAGE')
368 Tex
.extension
= 'CLIP'
370 Img
= bpy
.data
.images
.new(NameShort
, 128, 128)
372 Img
.filepath
= '%s%s%s' % (PathSaveRaw
, Name
, ExtSave
)
374 UVFace
= Active
.data
.uv_textures
[0].data
[0]
379 Mat
.texture_slots
.add()
380 TexSlot
= Mat
.texture_slots
[0]
381 TexSlot
.texture
= Tex
382 TexSlot
.texture_coords
= 'UV'
386 Tex
= bpy
.data
.textures
.new(NameShort
+'_A', 'IMAGE')
387 Tex
.extension
= 'CLIP'
388 Tex
.use_preview_alpha
= True
390 Img
= bpy
.data
.images
.new(NameShort
+'_A', 128, 128)
392 Img
.alpha_mode
= AlphaMode
393 Img
.filepath
= '%s%s_A%s' % (PathSaveRaw
, Name
, ExtSave
)
394 Img
.use_alpha
= False
398 Mat
.texture_slots
.add()
399 TexSlot
= Mat
.texture_slots
[1]
400 TexSlot
.texture
= Tex
401 TexSlot
.use_map_alpha
= True
402 TexSlot
.use_map_color_diffuse
= False
403 TexSlot
.texture_coords
= 'UV'
404 if OpacityMode
== 'TEX': TexSlot
.alpha_factor
= LayerOpacity
405 elif OpacityMode
== 'MAT': TexSlot
.blend_type
= 'MULTIPLY'
407 if not Active
.material_slots
:
408 bpy
.ops
.object.material_slot_add()
410 Active
.material_slots
[0].material
= Mat
420 Make3DLayer(Layer
['LayerName'].replace(' ', '_'),
421 Layer
['LayerNameShort'].replace(' ', '_'),
423 Layer
['LayerCoords'],
424 Layer
['RenderLayer'],
426 Layer
['LayerOpacity'],
433 #-------------------------------------------------
436 Scene
.use_nodes
= True
438 Tree
= Scene
.node_tree
446 LayerLen
= len(LayerList
)
448 for Layer
in LayerList
:
452 X_Offset
= (500*Offset
)
453 Y_Offset
= (-300*Offset
)
455 Node
= Tree
.nodes
.new('R_LAYERS')
456 Node
.location
= (-500+X_Offset
, 300+Y_Offset
)
457 Node
.name
= 'R_'+ str(Offset
)
459 Node
.layer
= Layer
[0]
462 Node_V
= Tree
.nodes
.new('VIEWER')
463 Node_V
.name
= Layer
[0]
464 Node_V
.location
= (-200+X_Offset
, 200+Y_Offset
)
466 Tree
.links
.new(Node
.outputs
[0], Node_V
.inputs
[0])
468 if LayerLen
> Offset
:
470 Mode
= LayerList
[Offset
][1] # has to go one step further
471 LayerOpacity
= LayerList
[Offset
][2]
473 if not Mode
in {'Normal', '-1'}:
475 Node
= Tree
.nodes
.new('MIX_RGB')
476 if OpacityMode
== 'COMPO': Node
.inputs
['Fac'].default_value
[0] = LayerOpacity
477 else: Node
.inputs
['Fac'].default_value
[0] = 1
478 Node
.use_alpha
= True
480 if Mode
in {'Addition', '7'}: Node
.blend_type
= 'ADD'
481 elif Mode
in {'Subtract', '8'}: Node
.blend_type
= 'SUBTRACT'
482 elif Mode
in {'Multiply', '3'}: Node
.blend_type
= 'MULTIPLY'
483 elif Mode
in {'DarkenOnly', '9'}: Node
.blend_type
= 'DARKEN'
484 elif Mode
in {'Dodge', '16'}: Node
.blend_type
= 'DODGE'
485 elif Mode
in {'LightenOnly', '10'}: Node
.blend_type
= 'LIGHTEN'
486 elif Mode
in {'Difference', '6'}: Node
.blend_type
= 'DIFFERENCE'
487 elif Mode
in {'Divide', '15'}: Node
.blend_type
= 'DIVIDE'
488 elif Mode
in {'Overlay', '5'}: Node
.blend_type
= 'OVERLAY'
489 elif Mode
in {'Screen', '4'}: Node
.blend_type
= 'SCREEN'
490 elif Mode
in {'Burn', '17'}: Node
.blend_type
= 'BURN'
491 elif Mode
in {'Color', '13'}: Node
.blend_type
= 'COLOR'
492 elif Mode
in {'Value', '14'}: Node
.blend_type
= 'VALUE'
493 elif Mode
in {'Saturation', '12'}: Node
.blend_type
= 'SATURATION'
494 elif Mode
in {'Hue', '11'}: Node
.blend_type
= 'HUE'
495 elif Mode
in {'Softlight', '19'}: Node
.blend_type
= 'SOFT_LIGHT'
499 Node
= Tree
.nodes
.new('ALPHAOVER')
500 if OpacityMode
== 'COMPO': Node
.inputs
['Fac'].default_value
[0] = LayerOpacity
501 Node
.name
= 'M_' + str(Offset
)
502 Node
.location
= (300+X_Offset
, 250+Y_Offset
)
505 Node_V
= Tree
.nodes
.new('VIEWER')
506 Node_V
.name
= Layer
[0]
507 Node_V
.location
= (500+X_Offset
, 350+Y_Offset
)
509 Tree
.links
.new(Node
.outputs
[0], Node_V
.inputs
[0])
512 Node
= Tree
.nodes
.new('COMPOSITE')
513 Node
.name
= 'Composite'
514 Node
.location
= (400+X_Offset
, 350+Y_Offset
)
516 Nodes
= bpy
.context
.scene
.node_tree
.nodes
519 for i
in range (1, LayerLen
+ 1):
521 Tree
.links
.new(Nodes
['R_'+str(i
)].outputs
[0], Nodes
['M_'+str(i
)].inputs
[1])
523 Tree
.links
.new(Nodes
['M_'+str(i
-1)].outputs
[0], Nodes
['M_'+str(i
)].inputs
[1])
524 if 1 < i
< LayerLen
+1:
525 Tree
.links
.new(Nodes
['R_'+str(i
)].outputs
[0], Nodes
['M_'+str(i
-1)].inputs
[2])
527 Tree
.links
.new(Nodes
['M_'+str(i
-1)].outputs
[0], Nodes
['Composite'].inputs
[0])
529 Tree
.links
.new(Nodes
['R_1'].outputs
[0], Nodes
['Composite'].inputs
[0])
532 i
.location
[0] += -250*Offset
533 i
.location
[1] += 150*Offset
535 #------------------------------------------------------------------------
538 from bpy
.props
import *
542 class GIMPImageToScene(bpy
.types
.Operator
):
544 bl_idname
= "import.gimp_image_to_scene"
545 bl_label
= "GIMP Image to Scene"
546 bl_description
= "Imports GIMP multilayer image files into 3D Scenes"
547 bl_options
= {'REGISTER', 'UNDO'}
549 filename
= StringProperty(name
="File Name",
550 description
="Name of the file")
551 directory
= StringProperty(name
="Directory",
552 description
="Directory of the file")
554 LayerViewers
= BoolProperty(name
="Layer Viewers",
555 description
="Add Viewer nodes to each Render Layer node",
558 MixerViewers
= BoolProperty(name
="Mixer Viewers",
559 description
="Add Viewer nodes to each Mix node",
562 AlphaMode
= EnumProperty(name
="Alpha Mode",
563 description
="Representation of alpha information in the RGBA pixels",
565 ('STRAIGHT', 'Texture Alpha Factor', 'Transparent RGB and alpha pixels are unmodified'),
566 ('PREMUL', 'Material Alpha Value', 'Transparent RGB pixels are multiplied by the alpha channel')),
569 ShadelessMats
= BoolProperty(name
="Shadeless Material",
570 description
="Set Materials as Shadeless",
573 OpacityMode
= EnumProperty(name
="Opacity Mode",
574 description
="Layer Opacity management",
576 ('TEX', 'Texture Alpha Factor', ''),
577 ('MAT', 'Material Alpha Value', ''),
578 ('COMPO', 'Mixer Node Factor', ''),
579 ('BAKE', 'Baked in Image Alpha', '')),
582 SetCamera
= BoolProperty(name
="Set Camera",
583 description
="Create an Ortho Camera matching image resolution",
586 SetupCompo
= BoolProperty(name
="Setup Node Compositing",
587 description
="Create a compositing node setup (will delete existing nodes)",
590 GroupUntagged
= BoolProperty(name
="Group Untagged",
591 description
="Layers with no tag go to a single Render Layer",
594 LayerOffset
= FloatProperty(name
="Layer Separation",
595 description
="Distance between each 3D Layer in the Z axis",
599 LayerScale
= FloatProperty(name
="Layer Scale",
600 description
="Scale pixel resolution by Blender units",
604 def draw(self
, context
):
608 box
.label('3D Layers:', icon
='SORTSIZE')
609 box
.prop(self
, 'SetCamera', icon
='OUTLINER_DATA_CAMERA')
610 box
.prop(self
, 'OpacityMode', icon
='GHOST')
611 if self
.OpacityMode
== 'COMPO' and self
.SetupCompo
== False:
612 box
.label('Tip: Enable Node Compositing', icon
='INFO')
613 box
.prop(self
, 'AlphaMode', icon
='IMAGE_RGB_ALPHA')
614 box
.prop(self
, 'ShadelessMats', icon
='SOLID')
615 box
.prop(self
, 'LayerOffset')
616 box
.prop(self
, 'LayerScale')
619 box
.label('Compositing:', icon
='RENDERLAYERS')
620 box
.prop(self
, 'SetupCompo', icon
='NODETREE')
622 box
.prop(self
, 'GroupUntagged', icon
='IMAGE_ZDEPTH')
623 box
.prop(self
, 'LayerViewers', icon
='NODE')
624 box
.prop(self
, 'MixerViewers', icon
='NODE')
626 def execute(self
, context
):
628 filename
= self
.filename
629 directory
= self
.directory
632 LayerViewers
= self
.LayerViewers
633 MixerViewers
= self
.MixerViewers
634 OpacityMode
= self
.OpacityMode
635 AlphaMode
= self
.AlphaMode
636 ShadelessMats
= self
.ShadelessMats
637 SetCamera
= self
.SetCamera
638 SetupCompo
= self
.SetupCompo
639 GroupUntagged
= self
.GroupUntagged
640 LayerOffset
= self
.LayerOffset
641 LayerScale
= self
.LayerScale
644 if filename
.endswith('.xcf'): Ext
= '.xcf'
645 elif filename
.endswith('.xjt'): Ext
= '.xjt'
649 main(filename
, directory
, LayerViewers
, MixerViewers
, LayerOffset
,
650 LayerScale
, OpacityMode
, AlphaMode
, ShadelessMats
,
651 SetCamera
, SetupCompo
, GroupUntagged
, Ext
)
653 self
.report({'ERROR'},"Selected file wasn't valid, try .xcf or .xjt")
657 def invoke(self
, context
, event
):
658 wm
= bpy
.context
.window_manager
659 wm
.fileselect_add(self
)
661 return {'RUNNING_MODAL'}
664 # Registering / Unregister
665 def menu_func(self
, context
):
666 self
.layout
.operator(GIMPImageToScene
.bl_idname
, text
="GIMP Image to Scene (.xcf, .xjt)", icon
='PLUGIN')
670 bpy
.utils
.register_module(__name__
)
672 bpy
.types
.INFO_MT_file_import
.append(menu_func
)
676 bpy
.utils
.unregister_module(__name__
)
678 bpy
.types
.INFO_MT_file_import
.remove(menu_func
)
681 if __name__
== "__main__":