Quake .map exporter for Blender3D

not sure if this helps… time for work sry, I’ll look further later…search patchdef…(q3map2 turns those coordinates into curve so subsurfing is not required)
quote:Originally posted by tomb4

Well,this should be a practical method but I doubt if quake3 does the same thing.Take a look at the entities.There are PatchDef which might be a suggestion for generating curved brushes?

Actually this is how Quake 3 does it… Take a look at the debug surfaces command (requires a map be loaded using /devmap). The engine creates “windings” (the technical name Q3 uses for clipped brush sides during the BSP compile) for each mesh at a very low tessellation factor and then collides vs. them.

PatchDef is the surface type q3map[2] uses during the BSP compile for bezier patches and specifies the control points and their dimensions. The original control points are really no different than the ones you see in the compiled BSP (except for vertex sharing). It’s equiv. to BrushDef for planar surfaces.

You can view the basic winding code Q3 and its tools use in polylib (in the GtkRadiant CVS tree under libs). A side note, some of the floating point math is not compatible with the winding_t logic unless compiled as C (most notably the intersection vs. 3 planes). It’s probably best just to write your own winding code :stuck_out_tongue:

On a final note, you might need to LoD some of the patches together (interpolate shared vertices for the edges) to avoid potentially sliding through patches when you transition from one to another.

I don’t think there is any docs on Quake 3 .map format :frowning: I will ask knowledgeable people from Quake community though :wink:

Cambo,

I just talked to the guy who created Dark Places engine (based on Quake engine, but as powerfull as Doom3 engine). He recommended to look at his hmap2 compiler and GTKr source. Two URLs are below:

http://cvs.icculus.org/cvs/twilight/hmap2/map.c?rev=1.8&view=auto

https://zerowing.idsoftware.com/svn/radiant/GtkRadiant/trunk/radiant/brushtokens.h

motorstep, what are you requesting?
The first links reference to BRUSHTYPE_PATCHDEF2 ignore it. the second link makes no referece to it.Im getting a bit discouraged because .MAP is not a standardized format, and everybody wants their own fav engines features supported.

If sombody wants to continue development Ill help out, or even add in spesific requests, but adding in vague undocumented features suck up too much time…

Also, theirs 2 existing opensource map making tools so its not like Blenders the only way to make a quake level.

Cambo,

Sad news :frowning: How can I encourage you to finish Quake 1 map exporter where lights, entities and proper scale has to be added (since we export brushes already correctly)? :wink:
No patches or other fancy stuff.

if you give clear requests Ill probably add them, Zenitor, for now Ill not worry about quake 3 features, if others want to add I dont mind.

Id just prefer somebody with an interest in this area work on the script, and I can help if they need - (request for maintainer of a simple script, anyone?)

motorsep: which Darkplaces engine are you mapping for? Darkplaces uses the .bsp and Lordhavoc added quake3 curve funtionality, a .map file compiled with q3map2 will create a .bsp that should work correctly in all Darkplaces engines, definitly does in nexuiz, checkout all the curved maps, seems to be the default style :slight_smile: , just tested a patchdef2 .map and works fine.

Cambo: this is not a quake3 specific request, after thinking about :slight_smile:
The q3map2 converts the .map into a bsp format that works in both
the Quake3 and Darkplaces(quake1) engines, and this is a core need,
I’ve just tested it out in nexuiz a Darkplaces engine, works as expected, any quake dirivative engine requires it for curved achitecture.

The q3map2 utility does all the bezier conversions, all that is required is the ability to convert a blender plane into the patchdef2 format, I"ll try to find more info but at this stage it appear that

// entity 0
{
“classname” “worldspawn”
// brush 0
{
patchDef2
{
NULL
( 3 3 0 0 0 ) where 3 3 defines a 3x3 vert plane
(
( ( 32 0 0 0 0 ) ( 32 0 32 0 -1 ) ( 32 0 64 0 -2 ) )
( ( 32 32 0 1 0 ) ( 32 32 32 1 -1 ) ( 32 32 64 1 -2 ) )
( ( 32 64 0 2 0 ) ( 32 64 32 2 -1 ) ( 32 64 64 2 -2 ) )
)
}
}
}
where first 3 digits are xyz coordinates for verts,
last 2 are texture positioning(not uv)
(gtk overwrites these so not sure if they are soo important, may be fine with just 0 0 as place holder?)YES-tested, in each bracket last 2 digits can be 0 0 placeholders :slight_smile:

example of mesh plane order and resultant patchdef2
http://myeye.homestead.com/files/meshorder.jpg

The main thing is modelling in the other map making tools is they are terrible for modelling, entities lighting etc they handle fine, but the layingout of brushes is horrible, this is why Blender would be 100x better to model in

Zenitor,
What I really wanted is to be able to export brushes, lights and entities. I have never thought about patches, since if I have to, I will add them in GTKr.
But if Cambo really have no interest in it, we can not force him to do .map exporter because he has alot on his plate.

Any help he can give is much appreciated, the thing is adding patches(bezier curve) is a real pain in gtk, but in blender we could easily use a plane, subsurf it to get the same effect as in the final engine(for visualizing only), but when exporting in the map file disregard the subsurf as the patchdef2 only needs the vert points and texture positioning, all the other things like entities and lights are easy to do in gtk, it’s the brushwork and basic modelling were blender excels, sry if I hijacked your thread :frowning:

I understand you point. But what is the advantage of using patches if you can use misc_model instead (model that curve in pleder and export it as .obj/.ase, and place it where you would want to place a patch)?

motorstep, can you tell me whats not alredy working, as far as I can see the lights and brushes should work ok.

motorsep: each individual .obj/.ase would need to be individually uvmapped, and you could end up with hundreds of them, take a hallway with an arched roof, as a patch you can use the standard brush texture alignment exactly the same as cubes, one click asignment in gtk… as apposed to uvmaping in blender.

Cambo’s export works nicely with brushes, open the .map in gtk and you can assign textures quickly and easily, whereas the .obj/.ase formats require pre uvmapping, meaning a hell of a lot of work in blender, uvmapping is only really needed for organic and irregular surfaces.

Excellent a .map exporter for Blender. :slight_smile:

I’ve been quickly going through the thread, and it seems that people are requesting extra features like to be able to have the script do everything GtkRadiant does.

I would recommend against having the script able to hold entities, models etc and textures. As this just complicates it.

What I would love to see is the script able to export just the enviroment, and change faces into either brushes or the more patch.

Textures can be added in using GtkRadiant, and extra models can also be added using GtkRadiant.

The whole point of using Blender would be the ability to create more complex enviroments easily, like bumpy cliffs, and stair cases and curved roads. Trying to do these things in GtkRadiant is very hard and frustrating, doing them in Blender is easy.

Then it looks like all we need is a proper scale we have to model in and on export, script would scale level accordingly, and patches.

motorstep, what to you think could be the ‘proper scale’ - the script currently scales up by 100, because blenders generaly is used to make smaller models then Quakes map files. from my tests 100 seemed a good match.

exporting a mesh as brushes is interesting, motorstep is happy not to do this, but IMHO it could speed up the development process of a map file. as long as the mesh was kept simple.

  • Cam

I will save Quake player model to .blend file in original scale and that’s what I ment by “scale level accordingly”. Scale accordingly to Quake units.

What do you mean? Are you talking about taking any mesh (e.g. Suzanne), taking each face of it and converting it to box?
I mean it’s good idea, but if boxes will intersect, we will have Z-buffer problems in Quake (glitches and stuff). Or leaks.

added a function to convert any non faces not a part of a cube, to export as brushes, 1 per face.
this means you can have suzanne, and some cubes in 1 mesh, and the cubes will export as brushes and suzanne will export 1 brush per face.

intersections should not be a problem, all sides of the cube are invisible except the one that is the face.
twosided faces are supported also, they will give some minor error if seen from behind because the brush must have some width, in most cases single sided brushes will be used.

Triangles are exported as brushes with 5 entrys, as opposed to 6, this seems to work.
Maybe I could export primitives of this shape also. (extruded triangle shape)


from Blender import *

def write_cube2brush(file, faces, PREF_SCALE):
    # comment only
    # file.write('// brush "%s", "%s"
' % (ob.name, ob.getData(name_only=1)))
    file.write('// brush from cube
{
')
    
    for f in faces:
        # from 4 verts this gets them in reversed order and only 3 of them
        # 0,1,2,3 -> 2,1,0
        for v in f.v[2::-1]:
            file.write('( %.8f %.8f %.8f ) ' % tuple(v.co*PREF_SCALE) )    
        
        try:    mode= f.mode
        except:    mode= 0
        
        if mode & Mesh.FaceModes.INVISIBLE:
            file.write('common/caulk')
        else:
            try:    image= f.image
            except:    image= None
            
            if image:    file.write(sys.splitext(sys.basepath(image.filename))[0])
            else:        file.write('NULL')
            
        # Texture stuff ignored for now
        file.write(' 0 0 0 0.5 0.5 1 1 1
')
    file.write('}
')


def write_face2brush(file, face, PREF_SCALE):
    image_text= 'NULL'
    
    try:    mode= face.mode
    except:    mode= 0
    
    if mode & Mesh.FaceModes.INVISIBLE:
        image_text= 'common/caulk'
    else:
        try:    image= face.image
        except:    image= None
        if image:    image_text = sys.splitext(sys.basepath(image.filename))[0]
    
    f_v= face.v
    file.write('// brush from face
{
')
    # front
    for v in f_v[2::-1]:
        file.write('( %.8f %.8f %.8f ) ' % tuple(v.co*PREF_SCALE) )
    file.write(image_text)
    # Texture stuff ignored for now
    file.write(' 0 0 0 0.5 0.5 1 1 1
')
    
    # back
    no= face.no * 0.01
    
    for v in f_v[:3]:
        file.write('( %.8f %.8f %.8f ) ' % tuple((v.co-no)*PREF_SCALE) )
    if mode & Mesh.FaceModes.TWOSIDE:
        file.write(image_text)
    else:
        file.write('common/caulk')
    
    # Texture stuff ignored for now
    file.write(' 0 0 0 0.5 0.5 1 1 1
')
    
    
    # sides.
    for v in ((tuple((f_v[0].co)*PREF_SCALE), tuple((f_v[1].co)*PREF_SCALE), tuple((f_v[1].co-no)*PREF_SCALE))):
        file.write( '( %.8f %.8f %.8f ) ' %  v )
    file.write('common/caulk')
    file.write(' 0 0 0 0.5 0.5 1 1 1
')
    
    for v in ((tuple((f_v[1].co)*PREF_SCALE), tuple((f_v[2].co)*PREF_SCALE), tuple((f_v[2].co-no)*PREF_SCALE))):
        file.write( '( %.8f %.8f %.8f ) ' %  v )
    file.write('common/caulk')
    file.write(' 0 0 0 0.5 0.5 1 1 1
')

    if len(f_v)==3: # Tri, it seemms tri brushes are supported.
        for v in ((tuple((f_v[2].co)*PREF_SCALE), tuple((f_v[0].co)*PREF_SCALE), tuple((f_v[0].co-no)*PREF_SCALE))):
            file.write( '( %.8f %.8f %.8f ) ' %  v )
        file.write('common/caulk')
        file.write(' 0 0 0 0.5 0.5 1 1 1
')
    
    else: # Quad
        for v in ((tuple((f_v[2].co)*PREF_SCALE), tuple((f_v[3].co)*PREF_SCALE), tuple((f_v[3].co-no)*PREF_SCALE))):
            file.write( '( %.8f %.8f %.8f ) ' %  v )
        file.write('common/caulk')
        file.write(' 0 0 0 0.5 0.5 1 1 1
')
        
        for v in ((tuple((f_v[3].co)*PREF_SCALE), tuple((f_v[0].co)*PREF_SCALE), tuple((f_v[0].co-no)*PREF_SCALE))):
            file.write( '( %.8f %.8f %.8f ) ' %  v )    
        file.write('common/caulk')
        file.write(' 0 0 0 0.5 0.5 1 1 1
')
    file.write('}
')
    
def mesh2cubes(me):
    Mesh.Mode(Mesh.SelectModes.VERTEX) # se we can rely on face selection
    me.sel= False # use selection to flag used verts.
    
    # Count the quad users to work out the cubes are in the mesh.
    vert_quad_users= [[] for i in xrange(len(me.verts))]
    for f in me.faces:
        if len(f) == 4:
            for v in f:
                vert_quad_users[v.index].append(f)
    
    face_cube_groups= []
    
    
    for vidx, vqusers in enumerate(vert_quad_users):
        v= me.verts[vidx]
        # Dont look at again.
        if len(vqusers) == 3 and not v.sel:
            BAD= False
            v.sel= True
            unique_faces= set() # should be a set, py2.3 compat grrr :/
            
            # We have 3 users so we could be a part of a cube.
            for f in vqusers:
                
                if len(f) != 4:
                    BAD= True
                    break
                
                # Add as a key of unique faces
                unique_faces.add(f.index)
                
                for nxt_face_v in f:
                    if not nxt_face_v.sel: # Not from the original faces
                        nxt_face_v.sel= True
                        for other_face in vert_quad_users[nxt_face_v.index]:
                            if len(other_face)!=4:
                                BAD= True
                                break
                            unique_faces.add(other_face.index)
                    
                    if  BAD or len(unique_faces) > 6:
                        break
                    
                    nxt_face_v_qusers = vert_quad_users[nxt_face_v.index]
                    if len(nxt_face_v_qusers)==3:
                        for nxt_face in nxt_face_v_qusers:
                            
                            if len(nxt_face)!=4:
                                BAD= True
                                break
                            
                            unique_faces.add(nxt_face.index)
                            
                            if len(unique_faces) > 6:
                                break
                    else:
                        BAD= True
                        break
                    
                    if BAD or len(unique_faces) > 6:
                        break
            
            if not BAD and len(unique_faces) == 6:
                faces = [me.faces[i] for i in unique_faces]
                if len(set([v.index for f in faces for v in f]))==8:
                    face_cube_groups.append( faces )
    Mesh.Mode(Mesh.SelectModes.FACE) # se we can rely on face selection
    return face_cube_groups


def write_node_map(file, ob):
    '''
    Writes the properties of an object (empty in this case)
    as a MAP node as long as it has the property name - classname
    returns True/False based on weather a node was written
    '''
    props= [(p.name, p.data) for p in ob.properties]
    
    IS_MAP_NODE= False
    for name, value in props:
        if name=='classname':
            IS_MAP_NODE= True
            break
        
    if not IS_MAP_NODE:
        return False
    
    # Write a node
    file.write('{
')
    for name_value in props:
        file.write('"%s" "%s"
' % name_value)
    file.write('}
')
    return True


def export_map(filepath):
    print 'Map Exporter 0.0'
    file= open(filepath, 'w')
    
    # header
    file.write('
// entity 0
')
    file.write('{
')
    file.write('"classname" "worldspawn"
')
    
    PREF_SCALE= 10
    
    obs_mesh= []
    obs_lamp= []
    obs_surf= []
    obs_empty= []
    
    dummy_mesh= Mesh.New()
    
    
    for ob in Object.GetSelected():
        type= ob.getType()
        if type == 'Mesh':        obs_mesh.append(ob)
        elif type == 'Lamp':    obs_lamp.append(ob)
        elif type == 'Surf':    obs_surf.append(ob)
        elif type == 'Empty':    obs_empty.append(ob)
    
    print '	writing cubes from meshes'
    for ob in obs_mesh:
        dummy_mesh.getFromObject(ob.name)
        
        # Is the object 1 cube? - object-is-a-brush
        if len(dummy_mesh.verts) == 8 and len(dummy_mesh.faces) == 6:
            # This is a box
            dummy_mesh.transform(ob.matrixWorld)
            write_cube2brush(file, dummy_mesh.faces, PREF_SCALE)
            
        else:
            face_groups= mesh2cubes(dummy_mesh)
            if face_groups:
                dummy_mesh.transform(ob.matrixWorld)
                for face_cube_group in face_groups:
                    write_cube2brush(file, face_cube_group, PREF_SCALE)
                dummy_mesh.sel= 0 # desel all
                for fg in face_groups:
                    for f in fg:
                        f.sel= True
                
            else:
                print '		found no cubes in mesh object:', ob.name
            
            for f in dummy_mesh.faces:
                if not f.sel:
                    write_face2brush(file, f, PREF_SCALE)
                
            #print 'warning, not exporting "%s" it is not a cube' % ob.name
            
    file.write('}
')
    dummy_mesh.verts= None
    
    print '	writing lamps'
    for ob in obs_lamp:
        print '		%s' % ob.name
        lamp= ob.data
        file.write('{
')
        file.write('"classname" "light"
')
        file.write('"light" "%.6f"
' % (lamp.dist* PREF_SCALE))
        file.write('"origin" "%.6f %.6f %.6f"
' % tuple(ob.getLocation('worldspace')))
        file.write('"_color" "%.6f %.6f %.6f"
' % tuple(lamp.col))
        file.write('"style" "0"
')
        file.write('}
')
    
    for ob in obs_surf:
        print ob
    
    
    print '	writing empty objects as nodes'
    for ob in obs_empty:
        if write_node_map(file, ob):
            print '		%s' % ob.name
        else:
            print '		ignoring %s' % ob.name
    

def main():
    Window.FileSelector(export_map, 'EXPORT MAP', '*.map')

if __name__ == '__main__': main()

I tried exporting Suzzane, but the model wouldnt appear in GtkRadiant 1.2.12. I also tried exporting a simple landscape but it would load in GtkRadaint either.

can you try load in QuaRK? it works for me,
(I use Quark in linux through wine)
gtkradient seems to want ‘version’ in the header but quark dosnt mind…

Hmm I’ll try downloading Quark, and run it under cedega(Hopefully). :slight_smile:

But could you try getting it runing under GtkRadiant - theres a .rpm of 1.5 version or a .run for 1.4 version - you can get it at www.qeradiant.com

If you could get it running proper under GtkRadiant, I would be very much appreciated. :smiley: