QuakeForge 0.7.0 released.

Discuss anything not covered by any of the other categories.
taniwha
Posts: 401
Joined: Thu Jan 14, 2010 7:11 am
Contact:

QuakeForge 0.7.0 released.

Post by taniwha »

QuakeForge 0.7.0 has been released.

Highlights:
  • OpenGL alias model shadow fixes.
  • MDL import/export addon for blender has been properly packaged.
  • GLSL fog fixes.
  • Instant console raise/lower.
  • noclip physics changes.
  • Intermission and level change fixes.
  • Automatic quick save backups.
  • QuakeC-VM improvements.
  • Up to 128 dlights.
For more details, see NEWS.
Leave others their otherness.
http://quakeforge.net/
goldenboy
Posts: 924
Joined: Fri Sep 05, 2008 11:04 pm
Location: Kiel
Contact:

Re: QuakeForge 0.7.0 released.

Post by goldenboy »

Congrats. I look forward to trying it.
qbism
Posts: 1236
Joined: Thu Nov 04, 2004 5:51 am
Contact:

Re: QuakeForge 0.7.0 released.

Post by qbism »

Now I want to learn blender. And not just for painting wings3d models. The only other option for direct MDL production including animation is MS3D, which has not been updated in a while.
Somewhere there's a Chithon replacement model and tutorial for blender 2.4. Anything newer out there for noobs?
leileilol
Posts: 2783
Joined: Fri Oct 15, 2004 3:23 am

Re: QuakeForge 0.7.0 released.

Post by leileilol »

No, because I didn't write one yet. I have finally gotten used to the 2.6x interface though, chances are I could, in the same condescending way I did before.

This also depends on how well this new MDL exporter works (i.e. not being annoying and anal about selecting, transformations/rot/scale of objects). I will test by exporting the angelyss/dark character.

EDIT: exporter crashes on 2.61.
i should not be here
qbism
Posts: 1236
Joined: Thu Nov 04, 2004 5:51 am
Contact:

Re: QuakeForge 0.7.0 released.

Post by qbism »

Code: Select all

"blender": (2, 6, 3),
from init.py, needs 2.63?
leileilol
Posts: 2783
Joined: Fri Oct 15, 2004 3:23 am

Re: QuakeForge 0.7.0 released.

Post by leileilol »

ARGH
that means i have to install yet another regressing version of Blender for one format


EDIT: yeah, i'd say this script is too immature, especially compared to ajmdl export for 2.4x

- demands pre-triangulation
- no animations (instead it's the current frame)
- produces a very broken mesh similar to the bad md2 exporter

Image

same mesh, old exporter, which does some comfortable things:
- exports what is selected
- triangulates on the fly
- can work from any origin, rotation, scale (no need to apply)
- bakes image textures in the mdl
- exports animations to the current frame

Image
i should not be here
taniwha
Posts: 401
Joined: Thu Jan 14, 2010 7:11 am
Contact:

Re: QuakeForge 0.7.0 released.

Post by taniwha »

re blender 2.63: I seem to remember some matrix changes between 2.62- and 2.63 (or was that an earlier version?). Anyway, working with bmesh was definitely an issue (the path to the face data changed).

leileilol: yes, it's immature, but this is the first feedback I've received on it.
  • demanding triangulation: this was actually a deliberate decision as I thought the user might want explicit control over how quads (or, I guess ngons these days) triangulate (what with our previous discussion on tris vs quads, this one surprised me).
  • no animations: not true :) Even frame groups, multiple skins and skin groups are supported. However, it's scripted. I you either look at write_text in import_mdl.py or import an animated model at look at the generated text file, you'll see the documentation for the format.
  • producing a broken mesh: that's a bug somewhere, definitely a bug. I'll check it out, but I suspect I might need to get a model off you for testing.
On to your feature list:
  • exports what is selected: QF's script exports the currently active mesh. For clarity, just what do you want? All selected meshes into the one object?
  • triangulates on the fly: I can certainly do that. The only reason I didn't is mentioned above. Should I make it optional? If so, what should be the default?
  • can work from any origin, rotation, scale (no need to apply): I'll have to double check, but I believe that's the case for QF's. Also, depends on just what you mean about rotation: do you want a rotated mesh to be exported in a rotated state? I can see all sorts of possibilities, especially in conjunction with the selection item.
  • bakes image textures in the mdl: something on my unwritten TODO list.
  • exports animations to the current frame: I'm not sure what you mean by "to the current frame". Step from frame 1 to current, exporting an mdl frame for each blender frame?
Anyway, I very much appreciate your feedback.

And in anticipation of some questions:
  • All mdl options are specifiable: see the "QF MDL" panel in the Object properties tab (box icon).
  • The above mentioned panel is where you specify the name of the script (blender text object). Unfortunately, you have to type it or cut and paste it: there's currently no way to do a suitable list box thingy (like materials etc) for custom properties.
  • Unless you're planning on QF-only models (yeah, right), don't bother with the 16-bit check-box.
I hope that helps (well, minus the mangled mesh bug: I'll see what I can do).
Leave others their otherness.
http://quakeforge.net/
taniwha
Posts: 401
Joined: Thu Jan 14, 2010 7:11 am
Contact:

Re: QuakeForge 0.7.0 released.

Post by taniwha »

leileilol: I've found the ajmdl script. That should help get some good functionality going :)
Leave others their otherness.
http://quakeforge.net/
qbism
Posts: 1236
Joined: Thu Nov 04, 2004 5:51 am
Contact:

Re: QuakeForge 0.7.0 released.

Post by qbism »

taniwha wrote:leileilol: I've found the ajmdl script. That should help get some good functionality going :)
There's a newer script than the one in the original package. May or may not be the absolute latest, but posted for posterity:

Code: Select all

#!BPY

"""
Name: 'MDL (.mdl)'
Blender: 243
Group: 'Export'
Tooltip: 'Export to QuakeI file format (.mdl)'
"""

__author__  = "Andrew Apted"
__url__     = ("http://openarena.ws")
__version__ = "0.62 2011-03-29"

__bpydoc__  = """\
This script exports a QuakeI file (MDL).
"""

import math
import struct
import Blender
import BPyMesh


#q_shared
MAX_QPATH = 64
MDL_IDENT = "IDPO"
MDL_VERSION = 6
MDL_MAX_SKINS = 64
MDL_MAX_VERTICES = 1024
MDL_MAX_TRIANGLES = 2048
MDL_MAX_FRAMES = 256
MDL_XYZ_SCALE = (1.0 / 64.0)
MDL_BLENDER_SCALE = (1.0 / 1.0)


# strips file type extension
def StripExtension(name):
	if name.find('.') != -1:
		name = name[:name.find('.')]
	return name
	
#q_math
def VectorLength(v):
	return math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])

def RadiusFromBounds(mins, maxs):
	corner = [0, 0, 0]
	a = 0
	b = 0
	
	for i in range(0, 3):
		a = abs(mins[i])
		b = abs(maxs[i])
		if a > b:
			corner[i] = a
		else:
			corner[i] = b

	return VectorLength(corner)


# our own logger class. it works just the same as a normal logger except
# all info messages get show. 
class Logger:
	def __init__(self, name):
		self.has_warnings = False
		self.has_errors = False
		self.has_critical = False
		self.message = ""
		self.name = name
		self.start = 0
		
		if name not in ( text.getName() for text in Blender.Text.Get() ):
			self.outtext = Blender.Text.New(name)
		else:
			self.outtext = Blender.Text.Get(name)
			self.outtext.clear()
		
		self.outtext.write("\n___START___\n\n")
	
	def log(self, type, msg, *args):
			self.message = type.ljust(10) + ":" + msg % args
			self.outtext.write(self.message+'\n')
			print self.message
	
	def info(self, msg, *args):
		self.log("info", msg, *args)
	
	def warning(self, msg, *args):
		self.log("warning", msg, *args)
		self.has_warnings = True
	
	def error(self, msg, *args):
		self.log("error", msg, *args)
		self.has_errors = True
		
	def critical(self, msg, *args):
		self.log("critical", msg, *args)
		self.has_errors = True
	
class BlenderGui:
	def __init__(self, log):
		text = ["A log has been written to a blender text window.",
			"Change this window type to a text window.", 
			"You will be able to select the file %s." % log.name ]
		
		text+=["Parsed in %i seconds"%(Blender.sys.time() - log.start)]
		
		if log.has_critical:
			text += ["There were critical errors!!!!"]
		elif log.has_errors:
			text += ["There were errors!"]
		elif log.has_warnings:
			text += ["There were warnings"]
		
		text.reverse()
		self.msg = text
		
		Blender.Draw.Register(self.gui, self.event, self.button_event)
		
	def gui(self,):
		quitbutton = Blender.Draw.Button("Exit", 1, 0, 0, 100, 20, "Close Window")
		
		y = 35
		
		for line in self.msg:
			Blender.BGL.glRasterPos2i(10,y)
			Blender.Draw.Text(line)
			y+=15
			
	def event(self,evt, val):
		if evt == Blender.Draw.ESCKEY:
			Blender.Draw.Exit()
			return
	
	def button_event(self,evt):
		if evt == 1:
			Blender.Draw.Exit()
			return


log = Logger("mdl_export_log")


#------------------------------------------------------------------------#


class q1ColorCache:
	def __init__(self, gamma, fullbright):
		self.gamma = gamma
		self.fullbright = fullbright
		self.cache = {}
		self.MakeGammaTab()
		if fullbright:
			self.color_range = range(0,256)
		else:
			self.color_range = range(0,224)
		self.palette = (
		(  0,   0,   0), ( 15,  15,  15), ( 31,  31,  31), ( 47,  47,  47), 
		( 63,  63,  63), ( 75,  75,  75), ( 91,  91,  91), (107, 107, 107), 
		(123, 123, 123), (139, 139, 139), (155, 155, 155), (171, 171, 171), 
		(187, 187, 187), (203, 203, 203), (219, 219, 219), (235, 235, 235), 
		( 15,  11,   7), ( 23,  15,  11), ( 31,  23,  11), ( 39,  27,  15), 
		( 47,  35,  19), ( 55,  43,  23), ( 63,  47,  23), ( 75,  55,  27), 
		( 83,  59,  27), ( 91,  67,  31), ( 99,  75,  31), (107,  83,  31), 
		(115,  87,  31), (123,  95,  35), (131, 103,  35), (143, 111,  35), 
		( 11,  11,  15), ( 19,  19,  27), ( 27,  27,  39), ( 39,  39,  51), 
		( 47,  47,  63), ( 55,  55,  75), ( 63,  63,  87), ( 71,  71, 103), 
		( 79,  79, 115), ( 91,  91, 127), ( 99,  99, 139), (107, 107, 151), 
		(115, 115, 163), (123, 123, 175), (131, 131, 187), (139, 139, 203), 
		(  0,   0,   0), (  7,   7,   0), ( 11,  11,   0), ( 19,  19,   0), 
		( 27,  27,   0), ( 35,  35,   0), ( 43,  43,   7), ( 47,  47,   7), 
		( 55,  55,   7), ( 63,  63,   7), ( 71,  71,   7), ( 75,  75,  11), 
		( 83,  83,  11), ( 91,  91,  11), ( 99,  99,  11), (107, 107,  15), 
		(  7,   0,   0), ( 15,   0,   0), ( 23,   0,   0), ( 31,   0,   0), 
		( 39,   0,   0), ( 47,   0,   0), ( 55,   0,   0), ( 63,   0,   0), 
		( 71,   0,   0), ( 79,   0,   0), ( 87,   0,   0), ( 95,   0,   0), 
		(103,   0,   0), (111,   0,   0), (119,   0,   0), (127,   0,   0), 
		( 19,  19,   0), ( 27,  27,   0), ( 35,  35,   0), ( 47,  43,   0), 
		( 55,  47,   0), ( 67,  55,   0), ( 75,  59,   7), ( 87,  67,   7), 
		( 95,  71,   7), (107,  75,  11), (119,  83,  15), (131,  87,  19), 
		(139,  91,  19), (151,  95,  27), (163,  99,  31), (175, 103,  35), 
		( 35,  19,   7), ( 47,  23,  11), ( 59,  31,  15), ( 75,  35,  19), 
		( 87,  43,  23), ( 99,  47,  31), (115,  55,  35), (127,  59,  43), 
		(143,  67,  51), (159,  79,  51), (175,  99,  47), (191, 119,  47), 
		(207, 143,  43), (223, 171,  39), (239, 203,  31), (255, 243,  27), 
		( 11,   7,   0), ( 27,  19,   0), ( 43,  35,  15), ( 55,  43,  19), 
		( 71,  51,  27), ( 83,  55,  35), ( 99,  63,  43), (111,  71,  51), 
		(127,  83,  63), (139,  95,  71), (155, 107,  83), (167, 123,  95), 
		(183, 135, 107), (195, 147, 123), (211, 163, 139), (227, 179, 151), 
		(171, 139, 163), (159, 127, 151), (147, 115, 135), (139, 103, 123), 
		(127,  91, 111), (119,  83,  99), (107,  75,  87), ( 95,  63,  75), 
		( 87,  55,  67), ( 75,  47,  55), ( 67,  39,  47), ( 55,  31,  35), 
		( 43,  23,  27), ( 35,  19,  19), ( 23,  11,  11), ( 15,   7,   7), 
		(187, 115, 159), (175, 107, 143), (163,  95, 131), (151,  87, 119), 
		(139,  79, 107), (127,  75,  95), (115,  67,  83), (107,  59,  75), 
		( 95,  51,  63), ( 83,  43,  55), ( 71,  35,  43), ( 59,  31,  35), 
		( 47,  23,  27), ( 35,  19,  19), ( 23,  11,  11), ( 15,   7,   7), 
		(219, 195, 187), (203, 179, 167), (191, 163, 155), (175, 151, 139), 
		(163, 135, 123), (151, 123, 111), (135, 111,  95), (123,  99,  83), 
		(107,  87,  71), ( 95,  75,  59), ( 83,  63,  51), ( 67,  51,  39), 
		( 55,  43,  31), ( 39,  31,  23), ( 27,  19,  15), ( 15,  11,   7), 
		(111, 131, 123), (103, 123, 111), ( 95, 115, 103), ( 87, 107,  95), 
		( 79,  99,  87), ( 71,  91,  79), ( 63,  83,  71), ( 55,  75,  63), 
		( 47,  67,  55), ( 43,  59,  47), ( 35,  51,  39), ( 31,  43,  31), 
		( 23,  35,  23), ( 15,  27,  19), ( 11,  19,  11), (  7,  11,   7), 
		(255, 243,  27), (239, 223,  23), (219, 203,  19), (203, 183,  15), 
		(187, 167,  15), (171, 151,  11), (155, 131,   7), (139, 115,   7), 
		(123,  99,   7), (107,  83,   0), ( 91,  71,   0), ( 75,  55,   0), 
		( 59,  43,   0), ( 43,  31,   0), ( 27,  15,   0), ( 11,   7,   0), 
		(  0,   0, 255), ( 11,  11, 239), ( 19,  19, 223), ( 27,  27, 207), 
		( 35,  35, 191), ( 43,  43, 175), ( 47,  47, 159), ( 47,  47, 143), 
		( 47,  47, 127), ( 47,  47, 111), ( 47,  47,  95), ( 43,  43,  79), 
		( 35,  35,  63), ( 27,  27,  47), ( 19,  19,  31), ( 11,  11,  15), 
		( 43,   0,   0), ( 59,   0,   0), ( 75,   7,   0), ( 95,   7,   0), 
		(111,  15,   0), (127,  23,   7), (147,  31,   7), (163,  39,  11), 
		(183,  51,  15), (195,  75,  27), (207,  99,  43), (219, 127,  59), 
		(227, 151,  79), (231, 171,  95), (239, 191, 119), (247, 211, 139), 
		(167, 123,  59), (183, 155,  55), (199, 195,  55), (231, 227,  87), 
		(127, 191, 255), (171, 231, 255), (215, 255, 255), (103,   0,   0), 
		(139,   0,   0), (179,   0,   0), (215,   0,   0), (255,   0,   0), 
		(255, 243, 147), (255, 247, 199), (255, 255, 255), (159,  91,  83))
	#
	def MakeGammaTab(self):
		self.gamma_map = {}
		for r in range(0,256):
			k = r / 255.0
			k = k ** (1.0 / self.gamma)
			self.gamma_map[r] = int(k * 255.0)
	#
	def LookupColor(self, r, g, b):
		# AJA: find the closest color
		# (this might be better done in HSV space, giving more weight
		#  to the HUE and less weight to the VALUE).
   		r = self.gamma_map[r]
   		g = self.gamma_map[g]
   		b = self.gamma_map[b]
		#
		best = 0
		best_dist = 99999999
		#
		for i in self.color_range:
			dr = self.palette[i][0] - r
			dg = self.palette[i][1] - g
			db = self.palette[i][2] - b

			dist = dr*dr + dg*dg + db*db
			if dist == 0:  # exact match
				return i

			if dist < best_dist:
				best = i
				best_dist = dist

		return best

	def MapColor(self, r, g, b):
		rgb = r*65536 + g*256 + b

		if self.cache.has_key(rgb):
			return self.cache[rgb]

		#
		# don't let the cache grow without limit
		if len(self.cache) > 8000:
			self.cache.clear()

		pixel = self.LookupColor(r, g, b)

		self.cache[rgb] = pixel
		return pixel


class q1Normalizer:
	def __init__(self):
		self.group_range = range(0,11)
		self.x_group = (
		(1.0000, 0.0000, 0.0000), (52,52,52,52,143,143,143,143),
		(0.9554, 0.2952, 0.0000), (51,51,55,55,141,141,145,145),
		(0.9511, 0.1625, 0.2629), (53,63,57,70,142,148,146,151),
		(0.8642, 0.4429, 0.2389), (46,61,56,69,19,147,123,150),
		(0.8507, 0.5257, 0.0000), (41,41,54,54,18,18,116,116),
		(0.8507, 0.0000, 0.5257), (60,67,60,67,144,155,144,155),
		(0.8090, 0.3090, 0.5000), (48,62,58,68,16,149,124,152),
		(0.7166, 0.6817, 0.1476), (42,43,111,100,20,25,118,117),
		(0.6882, 0.5878, 0.4253), (47,76,140,101,21,156,125,161),
		(0.6817, 0.1476, 0.7166), (49,65,59,66,15,153,126,154),
		(0.5878, 0.4253, 0.6882), (50,75,139,102,17,157,128,160) )
		#
		self.y_group = (
		(0.0000, 1.0000, 0.0000), (32,32,104,104,32,32,104,104),
		(0.0000, 0.9554, 0.2952), (33,30,107,103,33,30,107,103),
		(0.2629, 0.9511, 0.1625), (36,39,109,105,34,31,122,115),
		(0.2389, 0.8642, 0.4429), (35,38,108,97,23,29,121,113),
		(0.5257, 0.8507, 0.0000), (44,44,112,112,27,27,119,119),
		(0.0000, 0.8507, 0.5257), (6,28,106,90,6,28,106,90),
		(0.5000, 0.8090, 0.3090), (37,40,110,98,22,26,120,114),
		(0.1476, 0.7166, 0.6817), (8,71,136,92,7,77,130,91),
		(0.4253, 0.6882, 0.5878), (45,73,138,99,24,158,131,159),
		(0.7166, 0.6817, 0.1476), (42,43,111,100,20,25,118,117),
		(0.6882, 0.5878, 0.4253), (47,76,140,101,21,156,125,161) )
		#
		self.z_group = (
		(0.0000, 0.0000, 1.0000), (5,84,5,84,5,84,5,84),
		(0.2952, 0.0000, 0.9554), (12,85,12,85,2,82,2,82),
		(0.1625, 0.2629, 0.9511), (14,86,134,96,4,83,132,89),
		(0.4429, 0.2389, 0.8642), (13,74,133,95,1,81,127,87),
		(0.5257, 0.0000, 0.8507), (11,64,11,64,0,80,0,80),
		(0.0000, 0.5257, 0.8507), (9,79,137,93,9,79,137,93),
		(0.3090, 0.5000, 0.8090), (10,72,135,94,3,78,129,88),
		(0.6817, 0.1476, 0.7166), (49,65,59,66,15,153,126,154),
		(0.5878, 0.4253, 0.6882), (50,75,139,102,17,157,128,160),
		(0.1476, 0.7166, 0.6817), (8,71,136,92,7,77,130,91),
		(0.4253, 0.6882, 0.5878), (45,73,138,99,24,158,131,159) )
	#
	def MapNormal(self, x, y, z):
		# AJA: I use the following shortcuts to speed up normal lookup:
		#      
		# Firstly, a preliminary match only uses the first quadrant
		# (where all coords are >= 0).  Then we use the appropriate
		# normal index for the actual quadrant.  We can do this because
		# the 162 MDL/MD2 normals are not arbitrary but are mirrored in
		# every quadrant.  The eight numbers in the lists above are the
		# indices for each quadrant (+++ ++- +-+ +-- -++ -+- --+ ---).
		#
		# Secondly we use the axis with the greatest magnitude (of the
		# incoming normal) to search an axis-specific group, which means
		# we only need to check about 1/3rd of the normals.
		#
		fx = abs(x)
		fy = abs(y)
		fz = abs(z)
		#
		group = self.x_group
		if (fy > fx) and (fy > fz):
			group = self.y_group
		elif (fz > fx) and (fz > fy):
			group = self.z_group
		#
		best = 0
		best_dot = -1
		#
		for i in self.group_range:
			dot = group[i*2][0] * fx + group[i*2][1] * fy + group[i*2][2] * fz
			#
			if dot > best_dot:
				best = i
				best_dot = dot
		#
		quadrant = 0
		if x < 0:
			quadrant += 4
		if y < 0:
			quadrant += 2
		if z < 0:
			quadrant += 1
		#
		return group[best*2+1][quadrant]


col_cache  = q1ColorCache(1.0, False)

normalizer = q1Normalizer()


#------------------------------------------------------------------------#


class mdlFrame:
	__slots__ = 'name', 'vlist', 'mins', 'maxs'

	def __init__(self, name, vlist):
		self.name  = name
		self.vlist = vlist

		# compute bbox of this frame
		mins = [ +9e9, +9e9, +9e9 ]
		maxs = [ -9e9, -9e9, -9e9 ]

		for i in range(0, 3):
			for V in vlist:
				if V[i] < mins[i]: mins[i] = V[i]
				if V[i] > maxs[i]: maxs[i] = V[i]

		self.mins = mins
		self.maxs = maxs


class mdlObject:
	def __init__(self):
		self.skins  = []
		self.verts  = []
		self.tris   = []
		self.frames = []
		#
		self.sync_type  = 0
		self.flags      = 0
		#
		self.skin_w = 32  # temp
		self.skin_h = 32
		self.avg_tri_size = 20.0
		#
		self.scale  = [1.0] * 3 # temp stuff
		self.origin = [0.0] * 3
		self.bound_radius = 100.0
		self.eye    = [0.5, 0.5, 0.5]
		#
		self.mins = [ +9e9, +9e9, +9e9 ]
		self.maxs = [ -9e9, -9e9, -9e9 ]

	def AddSkin(self, image):
		size = image.getSize()

		if len(self.skins) == 0:
			self.skin_w = size[0]
			self.skin_h = size[1]
		## FIXME: elif size[0] != self.skin_w  ERROR

		self.skins.append(image)

	def AddVertex(self, u, v): # returns new vertex index
		result = len(self.verts)

		# invert vertically
		v = 1 - v

		# map onto the skin
		if u < 0: u = 0
		if u > 1: u = 1
		if v < 0: v = 0
		if v > 1: v = 1

		u = int(u * 0.999 * self.skin_w + 0.99)
		v = int(v * 0.999 * self.skin_h + 0.99)
		on_seam = 0  ## FIXME

		self.verts.append( (on_seam, u, v) )
		return result
	
	def AddTriangle(self, vlist):
		on_front = 0
		self.tris.append( (on_front, vlist[0], vlist[2], vlist[1]) )
	
	def AddFrame(self, vlist):
		name = "frame_%d" % len(self.frames)
		f = mdlFrame(name, vlist)

		# update bbox of whole model
		for i in range(0, 3):
			if f.mins[i] < self.mins[i]: self.mins[i] = f.mins[i]
			if f.maxs[i] > self.maxs[i]: self.maxs[i] = f.maxs[i]

		self.frames.append(f)

	def CalcScaleOrigin(self):
		for i in range(0, 3):
			size = self.maxs[i] - self.mins[i]

			self.scale[i]  = size / 253.0
			self.origin[i] = self.mins[i] - 1.0 * self.scale[i]

		log.info("MDL Scale: (%1.3f, %1.3f, %1.3f)",
				 self.scale[0], self.scale[1], self.scale[2]);
		log.info("MDL Origin: (%1.3f, %1.3f, %1.3f)",
				 self.origin[0], self.origin[1], self.origin[2]);

	def MapCoord(self, x, y, z):
		x = int((x - self.origin[0]) / self.scale[0])
		y = int((y - self.origin[1]) / self.scale[1])
		z = int((z - self.origin[2]) / self.scale[2])

		return (x, y, z)

	def Write(self, file):
		self.CalcScaleOrigin()
		#
		# Header
		#
		file.write(struct.pack("<4si", MDL_IDENT, MDL_VERSION))
		#
		file.write(struct.pack("<3f3f4f",
				self.scale[0],  self.scale[1],  self.scale[2],
				self.origin[0], self.origin[1], self.origin[2],
				self.bound_radius,
				self.eye[0], self.eye[1], self.eye[2]))
		#
		file.write(struct.pack("<3i",
				1,  ##!!!!!!!!! FIXME
				self.skin_w,
				self.skin_h))
		#
		file.write(struct.pack("<5if",
				len(self.verts),
				len(self.tris),
				len(self.frames),
				self.sync_type,
				self.flags,
				self.avg_tri_size))
		#
		# Skins
		#
		if len(self.skins) == 0:
			self.WriteDummySkin(file)
		else:
			for S in self.skins:
				self.WriteSkin(file, S)
		#
		# Vertices (ST coords)
		self.WriteVertices(file)
		#
		# Triangles
		self.WriteTriangles(file)
		#
		# Frames
		self.WriteFrames(file)

	def WriteSkin(self, file, image):
		file.write(struct.pack("<i", 0))  # type = SINGLE

		x_range = range(0, self.skin_w)
		y_range = reversed(range(0, self.skin_h))

		for y in y_range:
			for x in x_range:
				rgb = image.getPixelI(x, y)
				pixel = col_cache.MapColor(rgb[0], rgb[1], rgb[2])
				file.write(struct.pack("B", pixel))

	def WriteDummySkin(self, file):
		file.write(struct.pack("<i", 0))  # type = SINGLE

		x_range = range(0, 16)
		y_range = range(0, 32)

		for y in y_range:
			for x in x_range:
				pix = 3 + 5 * ((x + int(y/2)) % 2)
				file.write(struct.pack("BB", pix, pix)) 

	def WriteVertices(self, file):
		for st in self.verts:
			file.write(
				struct.pack("<3i", st[0], st[1], st[2]))

	def WriteTriangles(self, file):
		for t in self.tris:
			file.write(
				struct.pack("<4i", t[0], t[1], t[2], t[3]))

	def WriteFrames(self, file):
		for f in self.frames:
			file.write(struct.pack("<i", 0))  # type = SINGLE
			#
			mins = self.MapCoord(f.mins[0], f.mins[1], f.mins[2])
			maxs = self.MapCoord(f.maxs[0], f.maxs[1], f.maxs[2])
			#
			file.write(struct.pack("<4B4B",
			           mins[0], mins[1], mins[2], 0,
			           maxs[0], maxs[1], maxs[2], 0))
			#
			file.write(struct.pack("16s", f.name))
			#
			# all vertices for this frame
			for V in f.vlist:
				xyz  = self.MapCoord(V[0], V[1], V[2])

				file.write(struct.pack("<4B",
					xyz[0], xyz[1], xyz[2], V[3]))


#------------------------------------------------------------------------#


def Export(fileName):
	#log starts here
	log.start = Blender.sys.time()
	
	log.info("Quake MDL Export script v0.62")
	
	if (fileName.find('.mdl', -4) <= 0):
		fileName += '.mdl'
	
	log.info("Exporting to: %s", fileName)
	
	# create MDL header object (contains everything else)
	mdl = mdlObject()

	# get the scene
	scene = Blender.Scene.GetCurrent()
	scene.makeCurrent()

	# find the mesh to process
	meshOBJ = None
	for obj in Blender.Object.GetSelected():
		# check if it's a mesh object
		if obj.getType() == "Mesh":
			meshOBJ = obj
			break

	if not meshOBJ:
		print "Error: Must select a mesh to output as MDL"
		Blender.Draw.PupMenu("Selected Object must be a mesh to output as MDL%t|OK")
		return
	
	scene.makeCurrent()

	total_frames = Blender.Get("curframe")

	Blender.Set("curframe", 1)
	Blender.Window.Redraw()

	# get the object (not just name) and the Mesh, not NMesh
	mesh = meshOBJ.getData(False, True)
	matrix = meshOBJ.getMatrix('worldspace')

	log.info("Materials: %s", mesh.materials)
	#if not mesh.materials:
	## surf.shaders[0].name = pathName + meshOBJ.name
	#else:
	## surf.shaders[0].name = pathName + mesh.materials[0].name

	# find skin texture
	mesh_image = mesh.faces[0].image

	if mesh_image == "":
		mesh_image = None

	if mesh_image:
		mdl.AddSkin(mesh_image)

	# Process the Mesh....

	# because MDL doesnt suppoort faceUVs like blender, we need to duplicate
	# any vertex that has multiple uv coords

	SeenVerts = {} # maps vertex id + UV coords to the new vertex id
	OldToNew  = {} # maps old vertex id to a list of new ids after duplicating to account for UV

	# process each face in the mesh
	for face in mesh.faces:
		
		# this makes a list for each tri in this face. a quad will be [[0,1,2],[0,2,3]]
		tris_in_this_face = []
		for vi in range(1, len(face)-1):
			tris_in_this_face.append([0, vi, vi + 1])
		
		# loop across each tri in the face, then each vertex in the tri
		for cur_tri in tris_in_this_face:

			tri_verts = []

			for vi in cur_tri:
				# get the old vertex index and uv coords
				old_vert = face.v[vi].index

				if mesh.faceUV == True:
					uv = tuple(face.uv[vi])
				elif mesh.vertexUV:
					uv = (face.v[vi].uvco[0], face.v[vi].uvco[1])
				else:
					uv = (0.0, 0.0) # handle case with no tex coords	

				if SeenVerts.has_key((old_vert, uv)):
					new_vert = SeenVerts[(old_vert, uv)]
				else:
					# haven't seen this vertex/uv combo before 
					new_vert = mdl.AddVertex(uv[0], uv[1])
					
					SeenVerts[(old_vert, uv)] = new_vert

					# now because we have created a new index, 
					# we need a way to link it to the index that
					# blender returns for NMVert.index
					if not OldToNew.has_key(old_vert):
						OldToNew[old_vert] = []

					OldToNew[old_vert].append(new_vert)

				tri_verts.append(new_vert)
			
			mdl.AddTriangle(tri_verts)

	# we're done with faces and uv coords

	# now vertices are stored as frames :-
	# all vertices for frame 1, all vertices for frame 2...., all vertices for frame n
	# so we need to iterate across blender's frames, and copy out each vertex
	for	frame_idx in range(0, total_frames):
		# print "Doing frame ", frame_idx, total_frames

		Blender.Set("curframe", frame_idx + 1)
		Blender.Window.Redraw()

		m = BPyMesh.getMeshFromObject(meshOBJ)
		m.transform(matrix)

		frame_verts = [ (0,0,0,0) ] * len(mdl.verts)

		no_face_verts = 0

		for V in m.verts:
			try:
				vlist = OldToNew[V.index]
			except:
				no_face_verts += 1
				continue

			norm = normalizer.MapNormal(V.no[0], V.no[1], V.no[2])

			vert_info = ( V.co[0], V.co[1], V.co[2], norm )

			# apply the position to all the duplicated vertices
			for new_vert in vlist:
				frame_verts[new_vert] = vert_info

		mdl.AddFrame(frame_verts)

		if frame_idx == 1 and no_face_verts > 0:
			log.warning("Found %d vertices not part of a face",
			            no_face_verts)

	# export!
	file = open(fileName, "wb")
	mdl.Write(file)
	file.close()
	
	log.info("Finished.")


def FileSelectorCallback(fileName):
	Export(fileName)
	BlenderGui(log)

Blender.Window.FileSelector(FileSelectorCallback, "Export Quake1 MDL", Blender.sys.makename(ext='.mdl'))

#--- editor settings ------------
# vi:ts=4:sw=4:noexpandtab
taniwha
Posts: 401
Joined: Thu Jan 14, 2010 7:11 am
Contact:

Re: QuakeForge 0.7.0 released.

Post by taniwha »

Ok, I've taken a brief look at ajmdl (ajmdl_export_055.py found on quaddicted). Here are some thoughts.
  • ajmdl processes only the one mesh (but does scan through all selected objects, stopping at the first mesh object).
  • Opps, I'd forgotten I hadn't yet implemented vertex normals. However, I doubt this is the source of your breakage.
  • For textures, ajmdl doesn't seem to do anything smarter than converting RGB to the quake palette. No multiple skins, no skin groups, and certainly nothing I'd call baking (my idea of baking being to take the uv islands from the various selected images and merging into the one image (would require careful uv layout by the artist, though).
  • I now understand what you mean by working with any origin/rotation/scale: ajmdl uses the vertices' world coordinates.
  • I now see also what you meant by "to the current frame" ('tis as I thought). Fair enough. I like my scheme, but in the absence of a control script, that makes for reasonable behavior.
I'll try to get an update out soon.

I need to think more about the idea of exporting multiple selected meshes to the one mdl file. The main issue is what to do about the mdl options as they're currently tied to the object (I could use the active object's settings).

[ninjaed]Ok, I've also looked at the version posted by qbism. The changes between 0.55 and 0.62 are limited to what looks like bug-fixes: inverting uv coords and the image, and changing something in the color cache. And then a few logging tweaks.
Leave others their otherness.
http://quakeforge.net/
taniwha
Posts: 401
Joined: Thu Jan 14, 2010 7:11 am
Contact:

Re: QuakeForge 0.7.0 released.

Post by taniwha »

Ok, I've done up a quick-and-dirty bugfix release (only the mdl addon). Look for "bugfix" in the QF downloads page.

Changes:
  • An exception when exporting a model with no image textures has been fixed
  • Vertex normals are now exported
  • Multiple vertices sharing the one UV coordinate now work as expected (leileilol: I believe this is your bug)
  • Quads and n-gons automatically get converted to triangles.
  • The model's location, rotation and scale are now non-destructively applied to the mesh (optional, default) before exporting.
  • If no frame information is available via the export script, Blender's frames 1 to the current frame (inclusive) will be exported as individual frames.
Leave others their otherness.
http://quakeforge.net/
leileilol
Posts: 2783
Joined: Fri Oct 15, 2004 3:23 am

Re: QuakeForge 0.7.0 released.

Post by leileilol »

taniwha wrote:Unless you're planning on QF-only models (yeah, right), don't bother with the 16-bit check-box.
Hexen II MDL?
taniwha wrote:I need to think more about the idea of exporting multiple selected meshes to the one mdl file. The main issue is what to do about the mdl options as they're currently tied to the object (I could use the active object's settings).
Noesis pulled it off somehow. Blender 2.4x had a "consolidate images into one" feature that I can't seem to find anymore in 2.6x.
i should not be here
taniwha
Posts: 401
Joined: Thu Jan 14, 2010 7:11 am
Contact:

Re: QuakeForge 0.7.0 released.

Post by taniwha »

I asked Campbell about image consolidation and I was told I'd have to write it myself (I may have asked the wrong question, though, and he didn't seem 100% certain). That said, it can't be that hard (though I imagine it will be quite slow written in python).

Did my changes fix most of your issues with the exporter?

I don't know anything about Hexen II MDL :(. The MD16 format was (I believe) designed by Serplord (Seth Galbraith).
Leave others their otherness.
http://quakeforge.net/
qbism
Posts: 1236
Joined: Thu Nov 04, 2004 5:51 am
Contact:

Re: QuakeForge 0.7.0 released.

Post by qbism »

leileilol wrote:
taniwha wrote:I need to think more about the idea of exporting multiple selected meshes to the one mdl file. The main issue is what to do about the mdl options as they're currently tied to the object (I could use the active object's settings).
Noesis pulled it off somehow. Blender 2.4x had a "consolidate images into one" feature that I can't seem to find anymore in 2.6x.
I've done multiple meshes in Wings3D and MS3D, but they were all mapped onto the same texture. Guessing that doesn't help for exporting a Q3 model + vwep.
Regarding export of multiple meshes would CTRL-J work in Blender (join meshes)? So far I can't tell if it "really" combines them or just groups them somehow. My attempt to export this way errored-out, forgot to do texture assignment instead of material assignment I think, but out of time for now...
taniwha
Posts: 401
Joined: Thu Jan 14, 2010 7:11 am
Contact:

Re: QuakeForge 0.7.0 released.

Post by taniwha »

ctrl-j does indeed truely join the meshes into the one object. However, I have no idea how it handles uvmaps and textures assigned via the uvmaps.
Leave others their otherness.
http://quakeforge.net/
Post Reply