Need help adding Doom 3 map support to Quake 3 map exporter in Blender.

Blender already has Quake 3 map exporter. I would like for someone to help adding support for Doom 3 map exporter. I have output of 2 .map files, one for Quake 3 and one for Doom 3. I can explain what exactly needs to be done to get data, but I have no clue what to use in Python API to get that data and write it to the file.

Anyone? Thanks.

NOTE: ideasman42 recommended using this math http://stackoverflow.com/questions/2096474/given-a-surface-normal-find-rotation-for-3d-plane for the task

Here is how .map file with a simple 256^3 units cube, with its origin at 0 0 0, looks like: http://www.pasteall.org/39912

Each line in the {} block represents an infinite plane (which would correspond to a plane on which Blender’s mesh quad/tris would lie). First 4 numbers in () are: “The 4 numbers forming the plane equation are nothing more than the normal vector of the plane and it’s Euclidean (shortest) distance to the origin.” The rest of the numbers can be hard coded for now. Blender’s material name should go in between " " (so material would have to be literally names as blah/blah/blah or similarly).

For those who are into deep technical details, here is a read: http://fabiensanglard.net/doom3/dmap.php

And finally Doom 3 map file format: http://web.archive.org/web/20091212230433/http://www.modwiki.net/wiki/MAP_(file_format)

looks much like idTech’s .map format, e.g. CoD:
http://wiki.modsrepository.com/index.php/Call_of_Duty_4:_.MAP_file_structure

Never fully understood how brushes are defined, but it’s easy to screw it up. Look at the last of the 3 components: a brush of size 64x32x32 or something at origin got a -8 there… If you set it 0, it will break.

Note that these engines support convex brushes only, so would have to do to something like this:
http://www.cgal.org/Manual/latest/doc_html/cgal_manual/Convex_decomposition_3/Chapter_main.html

While CoD was powered by idTech 3, heavily modified, its map format is different. Doom 3 format is close to Quake 3 format for which exporter is already in the system. The only fundamental difference is how brush’s sides-planes are represented.

I recall when ideasman42 was talking to me about specifics of the Quake 3 format, we decided to limit scene to brushes only.

Same goes for Doom 3. Limiting scene to convex meshes (designer would have to make sure all meshes are convex), ignoring patches and models. Only honor tris and quads. And limiting mesh to ~16 - 64 faces.

If you can make Doom 3 exporter, I can make a video, using Blender and a cube, explaining how it should works for Doom 3 map format.

The idea of having Quake 3 and Doom 3 map exporters not so much for making actual levels in Blender, because DoomRadian/DarkRadiant are much easier to make levels in (or assemble level out of models). Doom 3 map exporter is needed to export hull brushes, that are used for AI navigation and visibility culling. Especially for terrains. That explains why I don’t care for texture alignment or patches or other things, when it come to geometry of the level.

Besides geometry, there are entities (player, enemies, items, lights, etc.) I think Quake 3 exporter can export lights, which comes handy sometimes. So I think it’s a good idea to keep this feature for Doom 3 map exporter.

How do you define a plane with a normal and a distance from the origin? To define a plane you need a normal and a point on the plane. A normal and a distance describe an infinite number of planes tangent to the surface of a sphere.

If you have normal vector, you already have oriented infinite plane (as it will be always perpendicular to the normal vector). And the shortest distance from that plane (which will be a perpendicular line from the plane to 0 0 0) will define it’s location in space.

That’s pretty funky.

So the question is, how would you use a face in Blender to generate one of these plane definitions?

If that is the case then this script runs though the faces of a Mesh, calculates the shortest distance on that plane to the origin, and spits out a line with the output. The script assumes that all the faces in the mesh are “facing out”


import bpy
import mathutils
from mathutils import Vector


def export(obj):


    # Ensure this object is a mesh    
    mesh = obj.data
    if type(mesh) != bpy.types.Mesh:
        return
    
    # Get the bounding sphere for the object for ray-casting
    radius = -1
    for pt in obj.bound_box:
        
        vec = Vector( (pt[0], pt[1], pt[2]) )
        radius = max( radius, vec.length )
    
    # Make the ray casts, go just outside the bounding sphere
    radius = radius * 1.10
    
    # Iterate the faces and dump out the "Doom 3" definition which is
    # a normal + distance from the origin
    
    for f in mesh.faces:
        
        # Compute the distance to the mesh from the origin to the plane.
        
        # Line from origin in the direction of the face normal
        origin = Vector( (0, 0, 0) )
        target = f.normal.copy()
        target.normalize()
        target = target * radius


        # Find the target point.
        intersect = mathutils.geometry.intersect_line_plane(origin, target, mesh.vertices[f.vertices[0]].co, f.normal)


        # This should never happen, but I'll try to catch it anyway.        
        if intersect is None:
            print("( ERROR ) ");
            continue
        
        print("( %f %f %f %f ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) \"textures/axis_z_plus\" 0 0 0" % (f.normal.x, f.normal.y, f.normal.z, intersect.length) )
    


if __name__ == "__main__":
    
    print("-- RUNNING --")
    
    export(bpy.context.active_object)

It seems funky, but apparently it’s about internal architecture of the engine, as level designer sees brushes as basically meshes in DoomRadiant/DarkRadiant :slight_smile:

I wish I knew :slight_smile: My thinking was to grab mesh’s face, get normal vector from it using face’s normal. Then there gotta be a built-in to sorta represent face as infinite plane and then calculate distance to that plane, perpendicularly, from 0 0 0.

That’s exactly what my sample script does.

I tried running it on a cube of 256^3 units, so I could compare the output. Got error:

– RUNNING –
Traceback (most recent call last):
File “\Text”, line 55, in <module>
File “\Text”, line 27, in export
AttributeError: ‘Mesh’ object has no attribute ‘faces’
Error: Python script fail, look in the console for now…

Opps. I did that script on 2.62 which was before the B-mesh stuff came in. Try this one if you are on 2.63+


import bpy
import mathutils
from mathutils import Vector




def export(obj):




    # Ensure this object is a mesh    
    mesh = obj.data
    if type(mesh) != bpy.types.Mesh:
        return
    
    # Get the bounding sphere for the object for ray-casting
    radius = -1
    for pt in obj.bound_box:
        
        vec = Vector( (pt[0], pt[1], pt[2]) )
        radius = max( radius, vec.length )
    
    # Make the ray casts, go just outside the bounding sphere
    radius = radius * 1.10
    
    # Iterate the faces and dump out the "Doom 3" definition which is
    # a normal + distance from the origin
    
    for f in mesh.polygons:
        
        # Compute the distance to the mesh from the origin to the plane.
        
        # Line from origin in the direction of the face normal
        origin = Vector( (0, 0, 0) )
        target = f.normal.copy()
        target.normalize()
        target = target * radius




        # Find the target point.
        intersect = mathutils.geometry.intersect_line_plane(origin, target, mesh.vertices[f.vertices[0]].co, f.normal)




        # This should never happen, but I'll try to catch it anyway.        
        if intersect is None:
            print("( ERROR ) ");
            continue
        
        print("( %f %f %f %f ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) \"textures/axis_z_plus\" 0 0 0" % (f.normal.x, f.normal.y, f.normal.z, intersect.length) )
    




if __name__ == "__main__":
    
    print("-- RUNNING --")
    
    export(bpy.context.active_object)

Ok, ran it again:

– RUNNING –
( 0.000000 0.000000 -1.000000 128.000000 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_plus” 0 0 0
( 0.000000 -0.000000 1.000000 128.000000 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_plus” 0 0 0
( 1.000000 -0.000000 0.000000 127.999893 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_plus” 0 0 0
( -0.000000 -1.000000 -0.000000 127.999916 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_plus” 0 0 0
( -1.000000 0.000000 -0.000000 127.999939 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_plus” 0 0 0
( 0.000000 1.000000 0.000000 127.999924 ) ( ( 0.00390625 0 0.5 ) ( 0 0.003906250.5 ) ) “textures/axis_z_plus” 0 0 0

Here is the original:

( 0 0 1 -128 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_plus” 0 0 0
( 0 1 0 -128 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_y_plus” 0 0 0
( 1 0 0 -128 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_x_plus” 0 0 0
( 0 0 -1 -128 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_minus” 0 0 0
( 0 -1 0 -128 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_y_minus” 0 0 0
( -1 0 0 -128 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_x_minus” 0 0 0

Pretty close, needs more work though.

It fails to load into the level editor. So I manually edited values and got it loaded:

( 0.000000 0.000000 1.000000 -128.000000 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_plus” 0 0 0
( 0.000000 1.000000 0.000000 -128.000000 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_plus” 0 0 0
( 1.000000 0.000000 0.000000 -127.999893 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_plus” 0 0 0
( 0.000000 0.000000 -1.000000 -127.999916 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_plus” 0 0 0
( 0.000000 -1.000000 0.000000 -127.999939 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_plus” 0 0 0
( -1.000000 0.000000 0.000000 -127.999924 ) ( ( 0.00390625 0 0.5 ) ( 0 0.00390625 0.5 ) ) “textures/axis_z_plus” 0 0 0

So the order of things and proper value (negative or positive) is extremely important.

order is really important of course, see:
http://wiki.modsrepository.com/index.php/Call_of_Duty_2:d3dbsp#Lump.5B5.5D-_Brushsides

As I mentioned above, CoD’s map format is not the same as Doom 3 format, and order of sides is different.

I know this post is 1 year old, but is this script (Doom 3 map exporter for Blender) is finalized or abandoned?

Because it will serve me to create my scenes in Blender and export to the Doom3 engine (with a lot of work in DarkRadiant).

When I have time (not yet) I will begin to transform the script for the MAP Quake 3 format to Doom 3 format using the code of this post. At first I will focus only on the brushes (no patches) and creating entity by the name of the Blender object (eg info_player_start)

.

If anyone interested please let me know (we can work together).