+ libcxr_push_world_vmf.call( world, pointer(vmfinfo), m.fp )
+ libcxr_free_world.call( world )
+
+ return True
+
+ # World geometry
+ for brush in sceneinfo['geo']:
+ vmfinfo.visgroupid = int(brush['object'].cxr_data.visgroup)
+ if not _buildsolid( brush ):
+ cxr_batch_lines()
+ scene_redraw()
+ return False
+ vmfinfo.visgroupid = 0
+
+ libcxr_vmf_begin_entities.call(pointer(vmfinfo), m.fp)
+
+ # Entities
+ for ent in sceneinfo['entities']:
+ obj = ent['object']
+ ctx = ent['transform']
+ cls = ent['classname']
+
+ m.node( 'entity' )
+ m.kv( 'classname', cls )
+
+ kvs = cxr_entity_keyvalues( ent )
+
+ for kv in kvs:
+ if isinstance(kv[2], list):
+ m.kv( kv[0], ' '.join([str(_) for _ in kv[2]]) )
+ else: m.kv( kv[0], str(kv[2]) )
+
+ if obj == None:
+ pass
+ elif not isinstance( obj, bpy.types.Collection ):
+ if obj.type == 'MESH':
+ vmfinfo.visgroupid = int(obj.cxr_data.visgroup)
+ if not _buildsolid( ent ):
+ cxr_batch_lines()
+ scene_redraw()
+ return False
+
+ if obj != None:
+ m.node( 'editor' )
+ m.kv( 'visgroupid', str(obj.cxr_data.visgroup) )
+ m.kv( 'visgroupshown', '1' )
+ m.kv( 'visgroupautoshown', '1' )
+ m.edon()
+
+ m.edon()
+ vmfinfo.visgroupid = 0
+
+ print( "Done" )
+ return True
+
+# COmpile image using NBVTF and hash it (JOB HANDLER)
+#
+def compile_image(img):
+ if img==None:
+ return None
+
+ name = asset_name(img)
+ src_path = bpy.path.abspath(img.filepath)
+
+ dims = img.cxr_data.export_res
+ fmt = {
+ 'RGBA': NBVTF_IMAGE_FORMAT_ABGR8888,
+ 'DXT1': NBVTF_IMAGE_FORMAT_DXT1,
+ 'DXT5': NBVTF_IMAGE_FORMAT_DXT5,
+ 'RGB': NBVTF_IMAGE_FORMAT_BGR888
+ }[ img.cxr_data.fmt ]
+
+ mipmap = img.cxr_data.mipmap
+ lod = img.cxr_data.lod
+ clamp = img.cxr_data.clamp
+ flags = img.cxr_data.flags
+
+ q=bpy.context.scene.cxr_data.image_quality
+
+ userflag_hash = F"{mipmap}.{lod}.{clamp}.{flags}"
+ file_hash = F"{name}.{os.path.getmtime(src_path)}"
+ comphash = F"{file_hash}.{dims[0]}.{dims[1]}.{fmt}.{userflag_hash}.{q}"
+
+ if img.cxr_data.last_hash != comphash:
+ print( F"Texture update: {img.filepath}" )
+
+ src = src_path.encode('utf-8')
+ dst = (asset_full_path('materials',img)+'.vtf').encode('utf-8')
+
+ flags_full = flags
+
+ # texture setting flags
+ if not lod: flags_full |= NBVTF_TEXTUREFLAGS_NOLOD
+ if clamp:
+ flags_full |= NBVTF_TEXTUREFLAGS_CLAMPS
+ flags_full |= NBVTF_TEXTUREFLAGS_CLAMPT
+
+ if libnbvtf_convert.call(src,dims[0],dims[1],mipmap,fmt,q,flags_full,dst):
+ img.cxr_data.last_hash = comphash
+
+ return name
+
+#
+# Compile a material to VMT format. This is quick to run, doesnt need to be a
+# job handler.
+#
+def compile_material(mat):
+ info = material_info(mat)
+ properties = mat.cxr_data
+
+ print( F"Compile {asset_full_path('materials',mat)}.vmt" )
+ if properties.shader == 'Builtin':
+ return []
+
+ props = []
+
+ # Walk the property tree
+ def _mlayer( layer ):
+ nonlocal properties, props
+
+ for decl in layer:
+ if isinstance(layer[decl],dict): # $property definition
+ pdef = layer[decl]
+ ptype = pdef['type']
+
+ subdefines = False
+ default = None
+ prop = None
+
+ if 'shaders' in pdef and properties.shader not in pdef['shaders']:
+ continue
+
+ # Group expansion (does it have subdefinitions?)
+ for ch in pdef:
+ if isinstance(pdef[ch],dict):
+ subdefines = True
+ break
+
+ expandview = False
+
+ if ptype == 'ui':
+ expandview = True
+ else:
+ if ptype == 'intrinsic':
+ if decl in info:
+ prop = info[decl]
+ else:
+ prop = getattr(properties,decl)
+ default = pdef['default']
+
+ if not isinstance(prop,str) and \
+ not isinstance(prop,bpy.types.Image) and \
+ hasattr(prop,'__getitem__'):
+ prop = tuple([p for p in prop])
+
+ if prop != default:
+ # write prop
+ props += [(decl,pdef,prop)]
+
+ if subdefines:
+ expandview = True
+
+ if expandview: _mlayer(pdef)
+
+ _mlayer( cxr_shader_params )
+
+ # Write the vmt
+ with vdf_structure( F"{asset_full_path('materials',mat)}.vmt" ) as vmt:
+ vmt.node( properties.shader )
+ vmt.put( "// Convexer export\n" )
+
+ for pair in props:
+ decl = pair[0]
+ pdef = pair[1]
+ prop = pair[2]
+
+ def _numeric(v):
+ nonlocal pdef
+ if 'exponent' in pdef: return str(pow( v, pdef['exponent'] ))
+ else: return str(v)
+
+ if isinstance(prop,bpy.types.Image):
+ vmt.kv( decl, asset_name(prop))
+ elif isinstance(prop,bool):
+ vmt.kv( decl, '1' if prop else '0' )
+ elif isinstance(prop,str):
+ vmt.kv( decl, prop )
+ elif isinstance(prop,float) or isinstance(prop,int):
+ vmt.kv( decl, _numeric(prop) )
+ elif isinstance(prop,tuple):
+ vmt.kv( decl, F"[{' '.join([_numeric(_) for _ in prop])}]" )
+ else:
+ vmt.put( F"// (cxr) unkown shader value type'{type(prop)}'" )
+
+ vmt.edon()
+ return props
+
+def cxr_modelsrc_vphys( mdl ):
+ for obj in mdl.objects:
+ if obj.name == F"{mdl.name}_phy":
+ return obj
+ return None
+
+def cxr_export_modelsrc( mdl, origin, asset_dir, project_name, transform ):
+ dgraph = bpy.context.evaluated_depsgraph_get()
+
+ # Compute hash value
+ chash = asset_uid(mdl)+str(origin)+str(transform)
+
+ #for obj in mdl.objects:
+ # if obj.type != 'MESH':
+ # continue
+
+ # ev = obj.evaluated_get(dgraph).data
+ # srcverts=[(v.co[0],v.co[1],v.co[2]) for v in ev.vertices]
+ # srcloops=[(l.normal[0],l.normal[1],l.normal[2]) for l in ev.loops]
+
+ # chash=hashlib.sha224((str(srcverts)+chash).encode()).hexdigest()
+ # chash=hashlib.sha224((str(srcloops)+chash).encode()).hexdigest()
+
+ # if ev.uv_layers.active != None:
+ # uv_layer = ev.uv_layers.active.data
+ # srcuv=[(uv.uv[0],uv.uv[1]) for uv in uv_layer]
+ # else:
+ # srcuv=['none']
+
+ # chash=hashlib.sha224((str(srcuv)+chash).encode()).hexdigest()
+ # srcmats=[ ms.material.name for ms in obj.material_slots ]
+ # chash=hashlib.sha224((str(srcmats)+chash).encode()).hexdigest()
+ # transforms=[ obj.location, obj.rotation_euler, obj.scale ]
+ # srctr=[(v[0],v[1],v[2]) for v in transforms]
+ # chash=hashlib.sha224((str(srctr)+chash).encode()).hexdigest()
+
+ #if chash != mdl.cxr_data.last_hash:
+ # mdl.cxr_data.last_hash = chash
+ # print( F"Compile: {mdl.name}" )
+ #else:
+ # return True
+
+ bpy.ops.object.select_all(action='DESELECT')
+
+ # Get viewlayer
+ def _get_layer(col,name):
+ for c in col.children:
+ if c.name == name:
+ return c
+ sub = _get_layer(c,name)
+ if sub != None:
+ return sub
+ return None
+ layer = _get_layer(bpy.context.view_layer.layer_collection,mdl.name)
+
+ prev_state = layer.hide_viewport
+ layer.hide_viewport=False
+
+ # Collect materials to be compiled, and temp rename for export
+ mat_dict = {}
+
+ vphys = None
+ for obj in mdl.objects:
+ if obj.name == F"{mdl.name}_phy":
+ vphys = obj
+ continue
+
+ obj.select_set(state=True)
+ for ms in obj.material_slots:
+ if ms.material != None:
+ if ms.material not in mat_dict:
+ mat_dict[ms.material] = ms.material.name
+ ms.material.name = asset_uid(ms.material)
+ ms.material.use_nodes = False
+
+ uid=asset_uid(mdl)
+ bpy.ops.export_scene.fbx( filepath=F'{asset_dir}/{uid}_ref.fbx',\
+ check_existing=False,
+ use_selection=True,
+ apply_unit_scale=False,
+ bake_space_transform=False
+ )
+
+ bpy.ops.object.select_all(action='DESELECT')
+
+ if vphys != None:
+ vphys.select_set(state=True)
+ bpy.ops.export_scene.fbx( filepath=F'{asset_dir}/{uid}_phy.fbx',\
+ check_existing=False,
+ use_selection=True,
+ apply_unit_scale=False,
+ bake_space_transform=False
+ )
+ bpy.ops.object.select_all(action='DESELECT')
+
+ # Fix material names back to original
+ for mat in mat_dict:
+ mat.name = mat_dict[mat]
+ mat.use_nodes = True
+
+ layer.hide_viewport=prev_state
+
+ # Write out QC file
+ with open(F'{asset_dir}/{uid}.qc','w') as o:
+ o.write(F'$modelname "{project_name}/{uid}"\n')
+ #o.write(F'$scale .32\n')
+ o.write(F'$scale {transform["scale"]/100.0}\n')
+ o.write(F'$body _ "{uid}_ref.fbx"\n')
+ o.write(F'$staticprop\n')
+ o.write(F'$origin {origin[0]:.6f} {origin[1]:.6f} {origin[2]:.6f}\n')
+
+ if mdl.cxr_data.preserve_order:
+ o.write(F"$preservetriangleorder\n")
+
+ if mdl.cxr_data.texture_shadows:
+ o.write(F"$casttextureshadows\n")
+
+ o.write(F"$surfaceprop {mdl.cxr_data.surfaceprop}\n")
+
+ if vphys != None:
+ o.write(F'$collisionmodel "{uid}_phy.fbx"\n')
+ o.write("{\n")
+ o.write(" $concave\n")
+ o.write("}\n")
+
+ o.write(F'$cdmaterials {project_name}\n')
+ o.write(F'$sequence idle {uid}_ref.fbx\n')
+
+ return True
+#
+# Copy bsp file (and also lightpatch it)
+#
+def cxr_patchmap( src, dst ):
+ libcxr_lightpatch_bsp.call( src.encode('utf-8') )
+ shutil.copyfile( src, dst )
+ return True
+
+# Convexer operators
+# ------------------------------------------------------------------------------
+
+# Force reload of shared libraries
+#
+class CXR_RELOAD(bpy.types.Operator):
+ bl_idname="convexer.reload"
+ bl_label="Reload"
+ def execute(_,context):
+ shared_reload()
+ return {'FINISHED'}
+
+# Reset all debugging/ui information
+#
+class CXR_RESET(bpy.types.Operator):
+ bl_idname="convexer.reset"
+ bl_label="Reset Convexer"
+ def execute(_,context):
+ cxr_reset_all()
+ return {'FINISHED'}
+
+# Used for exporting data to use with ASAN builds
+#
+class CXR_DEV_OPERATOR(bpy.types.Operator):
+ bl_idname="convexer.dev_test"
+ bl_label="Export development data"
+
+ def execute(_,context):
+ # Prepare input data
+ mesh_src = mesh_cxr_format(context.active_object)
+ libcxr_write_test_data.call( pointer(mesh_src) )
+ return {'FINISHED'}
+
+class CXR_INIT_FS_OPERATOR(bpy.types.Operator):
+ bl_idname="convexer.fs_init"
+ bl_label="Initialize filesystem"
+
+ def execute(_,context):
+ gameinfo = F'{bpy.context.scene.cxr_data.subdir}/gameinfo.txt'
+
+ if libcxr_fs_set_gameinfo.call( gameinfo.encode('utf-8') ) == 1:
+ print( "File system ready" )
+ else:
+ print( "File system failed to initialize" )
+
+ return {'FINISHED'}
+
+def cxr_load_texture( path, is_normal ):
+ global cxr_asset_lib
+
+ if path in cxr_asset_lib['textures']:
+ return cxr_asset_lib['textures'][path]
+
+ print( F"cxr_load_texture( '{path}' )" )
+
+ pvtf = libcxr_fs_get.call( path.encode('utf-8'), 0 )
+
+ if not pvtf:
+ print( "vtf failed to load" )
+ cxr_asset_lib['textures'][path] = None
+ return None
+
+ x = c_int32(0)
+ y = c_int32(0)
+
+ img_data = libnbvtf_read.call( pvtf, pointer(x), pointer(y), \
+ c_int32(is_normal) )
+
+ x = x.value
+ y = y.value
+
+ if not img_data:
+ print( "vtf failed to decode" )
+ libcxr_fs_free.call( pvtf )
+ cxr_asset_lib['textures'][path] = None
+ return None
+
+ img_buf = gpu.types.Buffer('FLOAT', [x*y*4], [_/255.0 for _ in img_data[:x*y*4]])
+
+ tex = cxr_asset_lib['textures'][path] = \
+ gpu.types.GPUTexture( size=(x,y), layers=0, is_cubemap=False,\
+ format='RGBA8', data=img_buf )
+
+ libnbvtf_free.call( img_data )
+ libcxr_fs_free.call( pvtf )
+ return tex
+
+def cxr_load_material( path ):
+ global cxr_asset_lib
+
+ if path in cxr_asset_lib['materials']:
+ return cxr_asset_lib['materials'][path]
+
+ print( F"cxr_load_material( '{path}' )" )
+
+ pvmt = libcxr_valve_load_material.call( path.encode( 'utf-8') )
+
+ if not pvmt:
+ cxr_asset_lib['materials'][path] = None
+ return None
+
+ vmt = pvmt[0]
+ mat = cxr_asset_lib['materials'][path] = {}
+
+ if vmt.basetexture:
+ mat['basetexture'] = cxr_load_texture( vmt.basetexture.decode('utf-8'), 0)
+
+ if vmt.bumpmap:
+ mat['bumpmap'] = cxr_load_texture( vmt.bumpmap.decode('utf-8'), 1)
+
+ libcxr_valve_free_material.call( pvmt )
+
+ return mat
+
+def cxr_load_model_full( path ):
+ global cxr_asset_lib, cxr_mdl_shader
+
+ if path in cxr_asset_lib['models']:
+ return cxr_asset_lib['models'][path]
+
+ pmdl = libcxr_valve_load_model.call( path.encode( 'utf-8' ) )
+
+ print( F"cxr_load_model_full( '{path}' )" )
+
+ if not pmdl:
+ print( "Failed to load model" )
+ cxr_asset_lib['models'][path] = None
+ return None
+
+ mdl = pmdl[0]
+
+ # Convert our lovely interleaved vertex stream into, whatever this is.
+ positions = [ (mdl.vertex_data[i*8+0], \
+ mdl.vertex_data[i*8+1], \
+ mdl.vertex_data[i*8+2]) for i in range(mdl.vertex_count) ]
+
+ normals = [ (mdl.vertex_data[i*8+3], \
+ mdl.vertex_data[i*8+4], \
+ mdl.vertex_data[i*8+5]) for i in range(mdl.vertex_count) ]
+
+ uvs = [ (mdl.vertex_data[i*8+6], \
+ mdl.vertex_data[i*8+7]) for i in range(mdl.vertex_count) ]
+
+ fmt = gpu.types.GPUVertFormat()
+ fmt.attr_add(id="aPos", comp_type='F32', len=3, fetch_mode='FLOAT')
+ fmt.attr_add(id="aNormal", comp_type='F32', len=3, fetch_mode='FLOAT')
+ fmt.attr_add(id="aUv", comp_type='F32', len=2, fetch_mode='FLOAT')
+
+ vbo = gpu.types.GPUVertBuf(len=mdl.vertex_count, format=fmt)
+ vbo.attr_fill(id="aPos", data=positions )
+ vbo.attr_fill(id="aNormal", data=normals )
+ vbo.attr_fill(id="aUv", data=uvs )
+
+ batches = cxr_asset_lib['models'][path] = []
+
+ for p in range(mdl.part_count):
+ part = mdl.parts[p]
+ indices = mdl.indices[part.ibstart:part.ibstart+part.ibcount]
+ indices = [ (indices[i*3+0],indices[i*3+1],indices[i*3+2]) \
+ for i in range(part.ibcount//3) ]
+
+ ibo = gpu.types.GPUIndexBuf( type='TRIS', seq=indices )
+
+ batch = gpu.types.GPUBatch( type='TRIS', buf=vbo, elem=ibo )
+ batch.program_set( cxr_mdl_shader )
+
+ mat_str = cast( mdl.materials[ part.material ], c_char_p )
+ batches += [( cxr_load_material( mat_str.value.decode('utf-8') ), batch )]
+
+ libcxr_valve_free_model.call( pmdl )
+
+ return batches
+
+class CXR_LOAD_MODEL_OPERATOR(bpy.types.Operator):
+ bl_idname="convexer.model_load"
+ bl_label="Load model"
+
+ def execute(_,context):
+ global cxr_test_mdl, cxr_mdl_shader, cxr_asset_lib
+
+ cxr_test_mdl = cxr_load_model_full( bpy.context.scene.cxr_data.dev_mdl )
+
+ scene_redraw()
+ return {'FINISHED'}
+
+# UI: Preview how the brushes will looks in 3D view
+#
+class CXR_PREVIEW_OPERATOR(bpy.types.Operator):
+ bl_idname="convexer.preview"
+ bl_label="Preview Brushes"
+
+ RUNNING = False
+
+ def execute(_,context):
+ global cxr_view_mesh
+ global cxr_view_shader, cxr_view_mesh, cxr_error_inf
+
+ cxr_reset_all()
+
+ static = _.__class__
+
+ mesh_src = mesh_cxr_format(context.active_object)
+ world = cxr_decompose_globalerr( mesh_src )
+
+ if world == None:
+ return {'FINISHED'}
+
+ # Generate preview using cxr
+ #
+ ptrpreview = libcxr_world_preview.call( world )
+ preview = ptrpreview[0]
+
+ vertices = preview.vertices[:preview.vertex_count]
+ vertices = [(_[0],_[1],_[2]) for _ in vertices]
+
+ colours = preview.colours[:preview.vertex_count]
+ colours = [(_[0],_[1],_[2],_[3]) for _ in colours]
+
+ indices = preview.indices[:preview.indices_count]
+ indices = [ (indices[i*3+0],indices[i*3+1],indices[i*3+2]) \
+ for i in range(int(preview.indices_count/3)) ]
+
+ cxr_view_mesh = batch_for_shader(
+ cxr_view_shader, 'TRIS',
+ { "pos": vertices, "color": colours },
+ indices = indices,
+ )
+
+ libcxr_free_tri_mesh.call( ptrpreview )
+ libcxr_free_world.call( world )
+ cxr_batch_lines()
+ scene_redraw()
+
+ return {'FINISHED'}
+
+# Search for VMF compiler executables in subdirectory
+#
+class CXR_DETECT_COMPILERS(bpy.types.Operator):
+ bl_idname="convexer.detect_compilers"
+ bl_label="Find compilers"
+
+ def execute(self,context):
+ scene = context.scene
+ settings = scene.cxr_data
+ subdir = settings.subdir
+
+ for exename in ['studiomdl','vbsp','vvis','vrad']:
+ searchpath = os.path.normpath(F'{subdir}/../bin/{exename}.exe')
+ if os.path.exists(searchpath):
+ settings[F'exe_{exename}'] = searchpath
+
+ return {'FINISHED'}
+
+def cxr_compiler_path( compiler ):
+ settings = bpy.context.scene.cxr_data
+ subdir = settings.subdir
+ path = os.path.normpath(F'{subdir}/../bin/{compiler}.exe')
+
+ if os.path.exists( path ): return path
+ else: return None
+
+# Compatibility layer
+#
+def cxr_temp_file( fn ):
+ if CXR_GNU_LINUX == 1:
+ return F"/tmp/fn"
+ else:
+ filepath = bpy.data.filepath
+ directory = os.path.dirname(filepath)
+ return F"{directory}/{fn}"
+
+def cxr_winepath( path ):
+ if CXR_GNU_LINUX == 1:
+ return 'z:'+path.replace('/','\\')
+ else:
+ return path
+
+# Main compile function
+#
+class CXR_COMPILER_CHAIN(bpy.types.Operator):
+ bl_idname="convexer.chain"
+ bl_label="Compile Chain"
+
+ # 'static'
+ USER_EXIT = False
+ SUBPROC = None
+ TIMER = None
+ TIMER_LAST = 0.0
+ WAIT_REDRAW = False
+ FILE = None
+ LOG = []
+
+ JOBINFO = None
+ JOBID = 0
+ JOBSYS = None
+
+ def cancel(_,context):
+ #global cxr_jobs_batch
+ static = _.__class__
+ wm = context.window_manager
+
+ if static.SUBPROC != None:
+ static.SUBPROC.terminate()
+ static.SUBPROC = None
+
+ if static.TIMER != None:
+ wm.event_timer_remove( static.TIMER )
+ static.TIMER = None
+
+ static.FILE.close()
+
+ #cxr_jobs_batch = None
+ scene_redraw()
+ return {'FINISHED'}
+
+ def modal(_,context,ev):
+ static = _.__class__
+
+ if ev.type == 'TIMER':
+ global cxr_jobs_batch, cxr_error_inf
+
+ if static.WAIT_REDRAW:
+ scene_redraw()
+ return {'PASS_THROUGH'}
+ static.WAIT_REDRAW = True