+ return props
+
+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]} {origin[1]} {origin[2]}\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'}
+
+# 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'}
+
+# 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"
+
+ LASTERR = None
+ RUNNING = False
+
+ def execute(_,context):
+ return {'FINISHED'}
+
+ def modal(_,context,event):
+ global cxr_view_mesh
+ static = _.__class__
+
+ if event.type == 'ESC':
+ cxr_reset_lines()
+ cxr_batch_lines()
+ cxr_view_mesh = None
+ static.RUNNING = False
+
+ scene_redraw()
+ return {'FINISHED'}
+
+ return {'PASS_THROUGH'}
+
+ def invoke(_,context,event):
+ global cxr_view_shader, cxr_view_mesh
+ static = _.__class__
+ static.LASTERR = None
+
+ cxr_reset_lines()
+
+ mesh_src = mesh_cxr_format(context.active_object)
+
+ err = c_int32(0)
+ world = libcxr_decompose.call( mesh_src, pointer(err) )
+
+ if world == None:
+ cxr_view_mesh = None
+ cxr_batch_lines()
+ scene_redraw()
+
+ static.LASTERR = ["There is no error", \
+ "Non-Manifold",\
+ "Bad-Manifold",\
+ "No-Candidate",\
+ "Internal-Fail",\
+ "Non-Coplanar",\
+ "Non-Convex Polygon",\
+ "Bad Result"]\
+ [err.value]
+
+ if static.RUNNING:
+ return {'CANCELLED'}
+ else:
+ context.window_manager.modal_handler_add(_)
+ return {'RUNNING_MODAL'}
+
+ # 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()
+
+ # Allow user to spam the operator
+ if static.RUNNING:
+ return {'CANCELLED'}
+
+ if not static.RUNNING:
+ static.RUNNING = True
+ context.window_manager.modal_handler_add(_)
+ return {'RUNNING_MODAL'}
+
+# 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'}
+
+# 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
+
+ if static.WAIT_REDRAW:
+ scene_redraw()
+ return {'PASS_THROUGH'}
+ static.WAIT_REDRAW = True
+
+ if static.USER_EXIT:
+ print( "Chain USER_EXIT" )
+ return _.cancel(context)
+
+ if static.SUBPROC != None:
+ # Deal with async modes
+ status = static.SUBPROC.poll()
+ if status == None:
+
+ # Cannot redirect STDOUT through here without causing
+ # undefined behaviour due to the Blender Python specification.
+ #
+ # Have to write it out to a file and read it back in.
+ #
+ with open("/tmp/convexer_compile_log.txt","r") as log:
+ static.LOG = log.readlines()
+ return {'PASS_THROUGH'}
+ else:
+ #for l in static.SUBPROC.stdout:
+ # print( F'-> {l.decode("utf-8")}',end='' )
+ static.SUBPROC = None
+
+ if status != 0:
+ print(F'Compiler () error: {status}')
+ return _.cancel(context)
+
+ static.JOBSYS['jobs'][static.JOBID] = None
+ cxr_jobs_update_graph( static.JOBINFO )
+ scene_redraw()
+ return {'PASS_THROUGH'}
+
+ # Compile syncronous thing
+ for sys in static.JOBINFO:
+ for i,target in enumerate(sys['jobs']):
+ if target != None:
+
+ if callable(sys['exec']):
+ print( F"Run (sync): {static.JOBID} @{time.time()}" )
+
+ if not sys['exec'](*target):
+ print( "Job failed" )
+ return _.cancel(context)
+
+ sys['jobs'][i] = None
+ static.JOBID += 1
+ else:
+ # Run external executable (wine)
+ static.SUBPROC = subprocess.Popen( target,
+ stdout=static.FILE,\
+ stderr=subprocess.PIPE,\
+ cwd=sys['cwd'])
+ static.JOBSYS = sys
+ static.JOBID = i
+
+ cxr_jobs_update_graph( static.JOBINFO )
+ scene_redraw()
+ return {'PASS_THROUGH'}
+
+ # All completed
+ print( "All jobs completed!" )
+ cxr_jobs_batch = None
+
+ scene_redraw()
+ return _.cancel(context)
+
+ return {'PASS_THROUGH'}
+
+ def invoke(_,context,event):
+ static = _.__class__
+ wm = context.window_manager
+
+ if static.TIMER == None:
+ print("Launching compiler toolchain")
+
+ # Run static compilation units now (collect, vmt..)
+ filepath = bpy.data.filepath
+ directory = os.path.dirname(filepath)
+ settings = bpy.context.scene.cxr_data
+
+ asset_dir = F"{directory}/modelsrc"
+ material_dir = F"{settings.subdir}/materials/{settings.project_name}"
+ model_dir = F"{settings.subdir}/models/{settings.project_name}"
+ output_vmf = F"{directory}/{settings.project_name}.vmf"
+
+ os.makedirs( asset_dir, exist_ok=True )
+ os.makedirs( material_dir, exist_ok=True )
+ os.makedirs( model_dir, exist_ok=True )
+
+ static.FILE = open(F"/tmp/convexer_compile_log.txt","w")
+ static.LOG = []
+
+ sceneinfo = cxr_scene_collect()
+ image_jobs = []
+ qc_jobs = []
+
+ # Collect materials
+ a_materials = set()
+ for brush in sceneinfo['geo']:
+ for ms in brush['object'].material_slots:
+ a_materials.add( ms.material )
+ if ms.material.cxr_data.shader == 'VertexLitGeneric':
+ errmat = ms.material.name
+ errnam = brush['object'].name
+ print( F"Vertex shader {errmat} used on {errnam}")
+ return {'CANCELLED'}
+
+ a_models = set()
+ model_jobs = []
+ for ent in sceneinfo['entities']:
+ if ent['object'] == None: continue
+
+ if ent['classname'] == 'prop_static':
+ obj = ent['object']
+ if isinstance(obj,bpy.types.Collection):
+ target = obj
+ a_models.add( target )
+ model_jobs += [(target, ent['origin'], asset_dir, \
+ settings.project_name, ent['transform'])]
+ else:
+ target = obj.instance_collection
+ if target in a_models:
+ continue
+ a_models.add( target )
+
+ # TODO: Should take into account collection instancing offset
+ model_jobs += [(target, [0,0,0], asset_dir, \
+ settings.project_name, ent['transform'])]
+
+ elif ent['object'].type == 'MESH':
+ for ms in ent['object'].material_slots:
+ a_materials.add( ms.material )
+
+ for mdl in a_models:
+ uid = asset_uid(mdl)
+ qc_jobs += [F'{uid}.qc']
+
+ for obj in mdl.objects:
+ for ms in obj.material_slots:
+ a_materials.add( ms.material )
+ if ms.material.cxr_data.shader == 'LightMappedGeneric' or \
+ ms.material.cxr_data.shader == 'WorldVertexTransition':
+
+ errmat = ms.material.name
+ errnam = obj.name
+ print( F"Lightmapped shader {errmat} used on {errnam}")
+ return {'CANCELLED'}
+
+ # Collect images
+ for mat in a_materials:
+ for pair in compile_material(mat):
+ decl = pair[0]
+ pdef = pair[1]
+ prop = pair[2]
+
+ if isinstance(prop,bpy.types.Image):
+ flags = 0
+ if 'flags' in pdef: flags = pdef['flags']
+ if prop not in image_jobs:
+ image_jobs += [(prop,)]
+ prop.cxr_data.flags = flags
+
+ # Convexer jobs
+ static.JOBID = 0
+ static.JOBINFO = []
+
+ if settings.comp_vmf:
+ static.JOBINFO += [{
+ "title": "Convexer",
+ "w": 20,
+ "colour": (1.0,0.3,0.1,1.0),
+ "exec": cxr_export_vmf,
+ "jobs": [(sceneinfo,output_vmf)]
+ }]
+
+ if settings.comp_textures:
+ if len(image_jobs) > 0:
+ static.JOBINFO += [{
+ "title": "Textures",
+ "w": 40,
+ "colour": (0.1,1.0,0.3,1.0),
+ "exec": compile_image,
+ "jobs": image_jobs
+ }]
+
+ game = 'z:'+settings.subdir.replace('/','\\')
+ args = [ \
+ '-game', game, settings.project_name
+ ]
+
+ # FBX stage
+ if settings.comp_models:
+ if len(model_jobs) > 0:
+ static.JOBINFO += [{
+ "title": "Batches",
+ "w": 25,
+ "colour": (0.5,0.5,1.0,1.0),
+ "exec": cxr_export_modelsrc,
+ "jobs": model_jobs
+ }]
+
+ if len(qc_jobs) > 0:
+ static.JOBINFO += [{
+ "title": "StudioMDL",
+ "w": 20,
+ "colour": (0.8,0.1,0.1,1.0),
+ "exec": "studiomdl",
+ "jobs": [[settings[F'exe_studiomdl']] + [\
+ '-nop4', '-game', game, qc] for qc in qc_jobs],
+ "cwd": asset_dir
+ }]
+
+ # VBSP stage
+ if settings.comp_compile:
+ static.JOBINFO += [{
+ "title": "VBSP",
+ "w": 25,
+ "colour": (0.1,0.2,1.0,1.0),
+ "exec": "vbsp",
+ "jobs": [[settings[F'exe_vbsp']] + args],
+ "cwd": directory
+ }]
+
+ static.JOBINFO += [{
+ "title": "VVIS",
+ "w": 25,
+ "colour": (0.9,0.5,0.5,1.0),
+ "exec": "vvis",
+ "jobs": [[settings[F'exe_vvis']] + ['-fast'] + args ],
+ "cwd": directory
+ }]
+
+ vrad_opt = settings.opt_vrad.split()
+ static.JOBINFO += [{
+ "title": "VRAD",
+ "w": 25,
+ "colour": (0.9,0.2,0.3,1.0),
+ "exec": "vrad",
+ "jobs": [[settings[F'exe_vrad']] + vrad_opt + args ],
+ "cwd": directory
+ }]
+
+ static.JOBINFO += [{
+ "title": "CXR",
+ "w": 5,
+ "colour": (0.0,1.0,0.4,1.0),
+ "exec": cxr_patchmap,
+ "jobs": [(F"{directory}/{settings.project_name}.bsp",\
+ F"{settings.subdir}/maps/{settings.project_name}.bsp")]
+ }]
+
+ static.USER_EXIT=False
+ static.TIMER=wm.event_timer_add(0.1,window=context.window)
+ wm.modal_handler_add(_)
+
+ cxr_jobs_update_graph( static.JOBINFO )
+ scene_redraw()
+ return {'RUNNING_MODAL'}
+
+ print("Chain exiting...")
+ static.USER_EXIT=True
+ return {'RUNNING_MODAL'}
+
+class CXR_RESET_HASHES(bpy.types.Operator):
+ bl_idname="convexer.hash_reset"
+ bl_label="Reset asset hashes"
+
+ def execute(_,context):
+ for c in bpy.data.collections:
+ c.cxr_data.last_hash = F"<RESET>{time.time()}"
+ c.cxr_data.asset_id=0
+
+ for t in bpy.data.images:
+ t.cxr_data.last_hash = F"<RESET>{time.time()}"
+ t.cxr_data.asset_id=0
+
+ return {'FINISHED'}
+
+class CXR_COMPILE_MATERIAL(bpy.types.Operator):
+ bl_idname="convexer.matcomp"
+ bl_label="Recompile Material"
+
+ def execute(_,context):
+ active_obj = bpy.context.active_object
+ active_mat = active_obj.active_material
+
+ #TODO: reduce code dupe (L1663)
+ for pair in compile_material(active_mat):
+ decl = pair[0]
+ pdef = pair[1]
+ prop = pair[2]
+
+ if isinstance(prop,bpy.types.Image):
+ flags = 0
+ if 'flags' in pdef: flags = pdef['flags']
+ prop.cxr_data.flags = flags
+
+ compile_image( prop )
+
+ settings = bpy.context.scene.cxr_data
+ with open(F'{settings.subdir}/cfg/convexer_mat_update.cfg','w') as o:
+ o.write(F'mat_reloadmaterial {asset_name(active_mat)}')
+
+ # TODO: Move this
+ with open(F'{settings.subdir}/cfg/convexer.cfg','w') as o:
+ o.write('sv_cheats 1\n')
+ o.write('mp_warmup_pausetimer 1\n')
+ o.write('bot_kick\n')
+ o.write('alias cxr_reload "exec convexer_mat_update"\n')
+
+ return {'FINISHED'}
+
+# Convexer panels
+# ------------------------------------------------------------------------------
+
+# Helper buttons for 3d toolbox view
+#
+class CXR_VIEW3D( bpy.types.Panel ):
+ bl_idname = "VIEW3D_PT_convexer"
+ bl_label = "Convexer"
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_category = "Convexer"
+
+ @classmethod
+ def poll(cls, context):
+ return (context.object is not None)
+
+ def draw(_, context):
+ layout = _.layout
+ row = layout.row()
+ row.scale_y = 2
+ row.operator("convexer.preview")
+
+ if CXR_PREVIEW_OPERATOR.LASTERR != None:
+ box = layout.box()
+ box.label(text=CXR_PREVIEW_OPERATOR.LASTERR, icon='ERROR')
+
+# Main scene properties interface, where all the settings go
+#
+class CXR_INTERFACE(bpy.types.Panel):
+ bl_label="Convexer"
+ bl_idname="SCENE_PT_convexer"
+ bl_space_type='PROPERTIES'
+ bl_region_type='WINDOW'
+ bl_context="scene"
+
+ def draw(_,context):
+ _.layout.operator("convexer.reload")
+ _.layout.operator("convexer.dev_test")
+ _.layout.operator("convexer.preview")
+ _.layout.operator("convexer.hash_reset")
+
+ settings = context.scene.cxr_data
+
+ _.layout.prop(settings, "debug")
+ _.layout.prop(settings, "scale_factor")
+ _.layout.prop(settings, "skybox_scale_factor")
+ _.layout.prop(settings, "skyname" )
+ _.layout.prop(settings, "lightmap_scale")
+ _.layout.prop(settings, "light_scale" )
+ _.layout.prop(settings, "image_quality" )
+
+ box = _.layout.box()
+
+ box.prop(settings, "project_name")
+ box.prop(settings, "subdir")
+
+ box = _.layout.box()
+ box.operator("convexer.detect_compilers")
+ box.prop(settings, "exe_studiomdl")
+ box.prop(settings, "exe_vbsp")
+ box.prop(settings, "exe_vvis")
+ box.prop(settings, "exe_vrad")
+ box.prop(settings, "opt_vrad")
+
+ box = box.box()
+ row = box.row()
+ row.prop(settings,"comp_vmf")
+ row.prop(settings,"comp_textures")
+ row.prop(settings,"comp_models")
+ row.prop(settings,"comp_compile")
+
+ text = "Compile" if CXR_COMPILER_CHAIN.TIMER == None else "Cancel"
+ row = box.row()
+ row.scale_y = 3
+ row.operator("convexer.chain", text=text)
+