Blob Blame History Raw
#!BPY

""" 
Name: '3D Studio'
Blender: 233
Group: 'Import'
Tip: 'Import from 3DS file format. (.3ds)'
"""


######################################################
# 3ds Importer
# By:  Bob Holcomb and Richard Lärkäng
# Date: 22 APR 04
# Ver: 0.7
######################################################
# This script imports a 3ds file and the materials
#  into blender for editing.  Hopefully
# this will make it into a future version as an import
# feature of the menu.  Loader is based on 3ds loader
# from www.gametutorials.com(Thanks DigiBen).
######################################################

######################################################
# Importing modules
######################################################

import Blender
from Blender import NMesh, Scene, Object, Material
from Blender.BGL import *
from Blender.Draw import *
from Blender.Window import *
from Blender.Image import *
from Blender.Material import *

import sys, struct, string, types
from types import *

import os
from os import path


######################################################
# Data Structures
######################################################

#Some of the chunks that we will see
#----- Primary Chunk, at the beginning of each file
PRIMARY=19789 #0x4D4D

#------ Main Chunks
OBJECTINFO=15677	#0x3D3D				// This gives the version of the mesh and is found right before the material and object information
VERSION=2			#0x0002				// This gives the version of the .3ds file
EDITKEYFRAME=45056	#0xB000				// This is the header for all of the key frame info

#------ sub defines of OBJECTINFO
MATERIAL=45055		#0xAFFF				// This stored the texture info
OBJECT=16384		#0x4000				// This stores the faces, vertices, etc...

#------ sub defines of MATERIAL
MATNAME=40960		#0xA000				// This holds the material name
MATAMBIENT=40976	#0xA010
MATDIFFUSE=40992	#0xA020				// This holds the color of the object/material
MATSPECULAR=41008	#0xA030
MATMAP=41472		#0xA200				// This is a header for a new material
MATMAPFILE=41728	#0xA300				// This holds the file name of the texture

OBJECT_MESH=16640	#0x4100				// This lets us know that we are reading a new object

#------ sub defines of OBJECT_MESH
OBJECT_VERTICES=16656	#0x4110			// The objects vertices
OBJECT_FACES=16672		#0x4120			// The objects faces
OBJECT_MATERIAL=16688	#0x4130			// This is found if the object has a material, either texture map or color
OBJECT_UV=16704			#0x4140			// The UV texture coordinates
OBJECT_TRANS_MATRIX=16736	#0x4160		// The translation matrix of the object 54 bytes

#the chunk class
class chunk:
	ID=0
	length=0
	bytes_read=0

	#we don't read in the bytes_read, we compute that
	binary_format="<HI"


	def __init__(self):
		self.ID=0
		self.length=0
		self.bytes_read=0

	def dump(self):
		print "ID: ", self.ID
		print "ID in hex: ", hex(self.ID)
		print "length: ", self.length
		print "bytes_read: ", self.bytes_read


def read_chunk(file, chunk):
		temp_data=file.read(struct.calcsize(chunk.binary_format))
		data=struct.unpack(chunk.binary_format, temp_data)
		chunk.ID=data[0]
		chunk.length=data[1]
		#update the bytes read function
		chunk.bytes_read=6

		#if debugging
		#chunk.dump()

def read_string(file):
	s=""
	index=0
	#print "reading a string"
	#read in the characters till we get a null character
	temp_data=file.read(struct.calcsize("c"))
	data=struct.unpack("c", temp_data)
	s=s+(data[0])
	#print "string: ",s
	while(ord(s[index])!=0):
		index+=1
		temp_data=file.read(struct.calcsize("c"))
		data=struct.unpack("c", temp_data)
		s=s+(data[0])
		#print "string: ",s
	
	#remove the null character from the string
	the_string=s[:-1]
	return str(the_string)

######################################################
# IMPORT
######################################################
def process_next_object_chunk(file, previous_chunk, mesh):
	new_chunk=chunk()
	temp_chunk=chunk()

	while (previous_chunk.bytes_read<previous_chunk.length):
		#read the next chunk
		read_chunk(file, new_chunk)

		if (new_chunk.ID==OBJECT_MESH):
			print "Found an OBJECT_MESH chunk"
			print "object_mesh: lenght: ", new_chunk.length
			process_next_object_chunk(file, new_chunk, mesh)
			print "object mesh: bytes read: ", new_chunk.bytes_read

		elif (new_chunk.ID==OBJECT_VERTICES):
			print "Found an OBJECT_VERTICES chunk"
			print "object_verts: lenght: ", new_chunk.length
			temp_data=file.read(struct.calcsize("H"))
			data=struct.unpack("H", temp_data)
			new_chunk.bytes_read+=2
			num_verts=data[0]
			print "number of verts: ", num_verts
			for counter in range (0,num_verts):
				temp_data=file.read(struct.calcsize("3f"))
				new_chunk.bytes_read+=12 #3 floats x 4 bytes each
				data=struct.unpack("3f", temp_data)
				v=NMesh.Vert(data[0],data[1],data[2])
				mesh.verts.append(v)
			print "object verts: bytes read: ", new_chunk.bytes_read

		elif (new_chunk.ID==OBJECT_FACES):
			print "Found an OBJECT_FACES chunk"
			print "object faces: lenght: ", new_chunk.length
			temp_data=file.read(struct.calcsize("H"))
			data=struct.unpack("H", temp_data)
			new_chunk.bytes_read+=2
			num_faces=data[0]
			print "number of faces: ", num_faces

			for counter in range(0,num_faces):
				temp_data=file.read(struct.calcsize("4H"))
				new_chunk.bytes_read+=8 #4 short ints x 2 bytes each
				data=struct.unpack("4H", temp_data)
				#insert the mesh info into the faces, don't worry about data[3] it is a 3D studio thing
				f=NMesh.Face()
				f.v.append(mesh.verts[data[0]])
				f.v.append(mesh.verts[data[1]])
				f.v.append(mesh.verts[data[2]])
				mesh.faces.append(f)
			print "object faces: bytes read: ", new_chunk.bytes_read

		elif (new_chunk.ID==OBJECT_MATERIAL):
			print "Found an OBJECT_MATERIAL chunk"
			print "object material: length: ", new_chunk.length
			material_name=""
			material_name=str(read_string(file))
			#plus one for the null character that gets removed
			new_chunk.bytes_read+=(len(material_name)+1)
			print "material_name: ", material_name

			#look up the material in all the materials
			material_found=0
			for mat in Material.Get():
				
				#found it, add it to the mesh
				if(mat.name==material_name):
					if(len(mesh.materials)>=15):
						result=Blender.Draw.PupMenu("Cannot assign more than 16 materials to a mesh: Continue?%t|OK")
						break;
					else:
						mesh.addMaterial(mat)
						material_found=1
						print "found material: ",mat.name
						
						#figure out what material index this is for the mesh
						for mat_counter in range(0,len(mesh.materials)):
							if mesh.materials[mat_counter].name==material_name:
								mat_index=mat_counter
								print "material index: ",mat_index
						
						#break out of this for loop so we don't accidentally set material_found back to 0
						break
				else:
					material_found=0
					#print "Not matching: ", mat.name, " and ", material_name

			if(material_found==1):
				#read the number of faces using this material
				temp_data=file.read(struct.calcsize("H"))
				data=struct.unpack("H", temp_data)
				new_chunk.bytes_read+=2
				num_faces_using_mat=data[0]
				print "number of faces using this material: ", num_faces_using_mat

				#list of faces using mat
				for face_counter in range(0,num_faces_using_mat):
					temp_data=file.read(struct.calcsize("H"))
					new_chunk.bytes_read+=2
					data=struct.unpack("H", temp_data)
					#print "face #: ", data[0]
					mesh.faces[data[0]].materialIndex=mat_index
					
			else:
				#read past the information about the material you couldn't find
				print "Couldn't find material.  Reading past face material info"
				buffer_size=new_chunk.length-new_chunk.bytes_read
				binary_format=str(buffer_size)+"c"
				temp_data=file.read(struct.calcsize(binary_format))
				new_chunk.bytes_read+=buffer_size
			
			print "object mat: bytes read: ", new_chunk.bytes_read

		elif (new_chunk.ID==OBJECT_UV):
			print "Found an OBJECT_UV chunk"
			print "object uv: lenght: ", new_chunk.length
			temp_data=file.read(struct.calcsize("H"))
			data=struct.unpack("H", temp_data)
			new_chunk.bytes_read+=2
			num_uv=data[0]
			print "number of UV: ", num_uv

			for counter in range(0,num_uv):
				temp_data=file.read(struct.calcsize("2f"))
				new_chunk.bytes_read+=8 #2 float x 4 bytes each
				data=struct.unpack("2f", temp_data)
				#insert the insert the UV coords in the vertex data
				mesh.verts[counter].uvco[0]=data[0]
				mesh.verts[counter].uvco[1]=data[1]
			#turn on the sticky UV coords for this mesh
			mesh.hasVertexUV(1)
			print "object uv: bytes read: ", new_chunk.bytes_read

		else:
			print "Found some other Object chunk: ",hex(new_chunk.ID)
			print "object faces: length: ", new_chunk.length
			buffer_size=new_chunk.length-new_chunk.bytes_read
			binary_format=str(buffer_size)+"c"
			temp_data=file.read(struct.calcsize(binary_format))
			new_chunk.bytes_read+=buffer_size
			print "object other: bytes read: ", new_chunk.bytes_read

		previous_chunk.bytes_read+=new_chunk.bytes_read
		print "Bytes left in this Object chunk: ", previous_chunk.length-previous_chunk.bytes_read

def process_next_material_chunk(file, previous_chunk, mat):
	new_chunk=chunk()
	temp_chunk=chunk()

	while (previous_chunk.bytes_read<previous_chunk.length):
		#read the next chunk
		read_chunk(file, new_chunk)

		if (new_chunk.ID==MATNAME):
			print "Found a MATNAME chunk"
			material_name=""
			material_name=str(read_string(file))
			
			#plus one for the null character that ended the string
			new_chunk.bytes_read+=(len(material_name)+1)
			print "material_name: ", material_name
			
			mat.setName(material_name)
			print "mat.name: ", mat.name

		elif (new_chunk.ID==MATAMBIENT):
			print "Found a MATAMBIENT chunk"

			read_chunk(file, temp_chunk)
			temp_data=file.read(struct.calcsize("3B"))
			data=struct.unpack("3B", temp_data)
			temp_chunk.bytes_read+=3
			r=data[0]
			g=data[1]
			b=data[2]
			mat.setMirCol(float(r)/255, float(g)/255, float(b)/255)
			new_chunk.bytes_read+=temp_chunk.bytes_read

		elif (new_chunk.ID==MATDIFFUSE):
			print "Found a MATDIFFUSE chunk"

			read_chunk(file, temp_chunk)
			temp_data=file.read(struct.calcsize("3B"))
			data=struct.unpack("3B", temp_data)
			temp_chunk.bytes_read+=3
			r=data[0]
			g=data[1]
			b=data[2]
			mat.setRGBCol(float(r)/255, float(g)/255, float(b)/255)
			new_chunk.bytes_read+=temp_chunk.bytes_read

		elif (new_chunk.ID==MATSPECULAR):
			print "Found a MATSPECULAR chunk"

			read_chunk(file, temp_chunk)
			temp_data=file.read(struct.calcsize("3B"))
			data=struct.unpack("3B", temp_data)
			temp_chunk.bytes_read+=3
			r=data[0]
			g=data[1]
			b=data[2]
			mat.setSpecCol(float(r)/255, float(g)/255, float(b)/255)
			new_chunk.bytes_read+=temp_chunk.bytes_read

		elif (new_chunk.ID==MATMAP):
			print "Found a MATMAP chunk"
			#recurse into this one
			process_next_material_chunk(file, new_chunk, mat)

		elif (new_chunk.ID==MATMAPFILE):
			print "Found a MATMAPFILE chunk"
			texture_name=""
			texture_name=str(read_string(file))
			
			#plus one for the null character that gets removed
			new_chunk.bytes_read+=(len(texture_name)+1)

		else:
			print "Found some other Material chunk: ",hex(new_chunk.ID)
			buffer_size=new_chunk.length-new_chunk.bytes_read
			binary_format=str(buffer_size)+"c"
			temp_data=file.read(struct.calcsize(binary_format))
			new_chunk.bytes_read+=buffer_size

		previous_chunk.bytes_read+=new_chunk.bytes_read
		print "Bytes left in this Material chunk: ", previous_chunk.length-previous_chunk.bytes_read

def process_next_chunk(file, previous_chunk):
	#a spare chunk
	new_chunk=chunk()
	temp_chunk=chunk()

	#loop through all the data for this chunk (previous chunk) and see what it is
	while (previous_chunk.bytes_read<previous_chunk.length):
		#read the next chunk
		print "reading a chunk"
		read_chunk(file, new_chunk)

		#is it a Version chunk?
		if (new_chunk.ID==VERSION):
			print "found a VERSION chunk"
			print "version: lenght: ", new_chunk.length
			#read in the version of the file
			#it's an unsigned short (H)
			temp_data=file.read(struct.calcsize("I"))
			data=struct.unpack("I", temp_data)
			version=data[0]
			new_chunk.bytes_read+=4 #read the 4 bytes for the version number
			#this loader works with version 3 and below, but may not with 4 and above
			if (version>3):
				print "Non-Fatal Error:  Version greater than 3, may not load correctly: ", version

		#is it an object info chunk?
		elif (new_chunk.ID==OBJECTINFO):
			print "found an OBJECTINFO chunk"
			print "object info: lenght: ", new_chunk.length
			#recursively go through the rest of the file
			process_next_chunk(file, new_chunk)

			#keep track of how much we read in the main chunk
			new_chunk.bytes_read+=temp_chunk.bytes_read

		#is it an object chunk?
		elif (new_chunk.ID==OBJECT):
			print "found an OBJECT chunk"
			print "object length: ", new_chunk.length
			#make a mesh
			mesh=NMesh.New()
			mesh.name=str(read_string(file))
			print "mesh name: ", mesh.name
			#plus one for the null character that gets removed
			new_chunk.bytes_read+=(len(mesh.name)+1)

			process_next_object_chunk(file, new_chunk, mesh)

			#put the object into blender at the cursor location
			mesh_obj=NMesh.PutRaw(mesh)
			cursor_pos=Blender.Window.GetCursorPos()
			mesh_obj.setLocation(float(cursor_pos[0]),float(cursor_pos[1]),float(cursor_pos[2]))

		#is it a material chunk?
		elif (new_chunk.ID==MATERIAL):
			print "found a MATERIAL chunk"
			
			material=Material.New()
			process_next_material_chunk(file, new_chunk, material)

		else: #(new_chunk.ID!=VERSION or new_chunk.ID!=OBJECTINFO or new_chunk.ID!=OBJECT or new_chunk.ID!=MATERIAL):
			print "skipping to end of this chunk"
			buffer_size=new_chunk.length-new_chunk.bytes_read
			binary_format=str(buffer_size)+"c"
			temp_data=file.read(struct.calcsize(binary_format))
			new_chunk.bytes_read+=buffer_size


		#update the previous chunk bytes read
		previous_chunk.bytes_read+=new_chunk.bytes_read
		print "Bytes left in this chunk: ", previous_chunk.length-previous_chunk.bytes_read


def load_3ds (filename):

	current_chunk=chunk()
	
	file=open(filename,"rb")
	
	#here we go!
	print "reading the first chunk"
	read_chunk(file, current_chunk)
	if (current_chunk.ID!=PRIMARY):
		print "Fatal Error:  Not a valid 3ds file: ", filename
		Exit()

	process_next_chunk(file, current_chunk)

	file.close()


#***********************************************
# MAIN
#***********************************************

	
def my_callback(filename):
	load_3ds(filename)

Blender.Window.FileSelector(my_callback, "Import 3DS")