X-Git-Url: https://harrygodden.com/git/?p=convexer.git;a=blobdiff_plain;f=__init__.py;h=05aefbcb1066a2c5592a6c5d9f1ac5c3e55bd0c0;hp=763e6e3479032d2985a09a6c571b02c4501143b5;hb=HEAD;hpb=e09d3691cb55505c81624d93df7713dbbaafaa54 diff --git a/__init__.py b/__init__.py index 763e6e3..05aefbc 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,12 @@ -# Copyright (C) 2022 Harry Godden (hgn) +# CONVEXER v0.95 +# +# A GNU/Linux-first Source1 Hammer replacement +# built with Blender, for mapmakers +# +# Copyright (C) 2022 Harry Godden (hgn) +# +# LICENSE: GPLv3.0, please see COPYING and LICENSE for more information +# bl_info = { "name":"Convexer", @@ -20,6 +28,16 @@ from ctypes import * from gpu_extras.batch import batch_for_shader from bpy.app.handlers import persistent +# Setup platform dependent variables + +exec(open(F'{os.path.dirname(__file__)}/platform.py').read()) +if CXR_GNU_LINUX==1: + CXR_SHARED_EXT=".so" + CXR_EXE_EXT="" +else: + CXR_SHARED_EXT=".dll" + CXR_EXE_EXT=".exe" + # GPU and viewport drawing # ------------------------------------------------------------------------------ @@ -32,9 +50,19 @@ cxr_view_lines = None cxr_view_mesh = None cxr_jobs_batch = None cxr_jobs_inf = [] +cxr_error_inf = None +cxr_test_mdl = None + +cxr_asset_lib = \ +{ + "models": {}, + "materials": {}, + "textures": {} +} # Shaders cxr_view_shader = gpu.shader.from_builtin('3D_SMOOTH_COLOR') + cxr_ui_shader = gpu.types.GPUShader(""" uniform mat4 ModelViewProjectionMatrix; uniform float scale; @@ -59,16 +87,132 @@ void main() } """) +cxr_mdl_shader = gpu.types.GPUShader(""" +uniform mat4 modelMatrix; +uniform mat4 viewProjectionMatrix; + +in vec3 aPos; +in vec3 aNormal; +in vec2 aUv; + +out vec3 lPos; +out vec3 lNormal; +out vec2 lUv; + +void main() +{ + vec4 pWorldPos = modelMatrix * vec4(aPos, 1.0); + vec3 worldPos = pWorldPos.xyz; + + gl_Position = viewProjectionMatrix * pWorldPos; + lNormal = normalize(mat3(transpose(inverse(modelMatrix))) * aNormal); + lPos = worldPos; + lUv = aUv; +} +""",""" + +uniform vec4 colour; +uniform vec3 testLightDir; +uniform sampler2D uBasetexture; + +in vec3 lNormal; +in vec3 lPos; +in vec2 lUv; + +out vec4 FragColor; + +float SoftenCosineTerm( float flDot ) +{ + return ( flDot + ( flDot * flDot ) ) * 0.5; +} + +vec3 DiffuseTerm( vec3 worldNormal, vec3 lightDir ) +{ + float fResult = 0.0; + float NDotL = dot( worldNormal, lightDir ); + + fResult = clamp( NDotL, 0.0, 1.0 ); + fResult = SoftenCosineTerm( fResult ); + + vec3 fOut = vec3( fResult, fResult, fResult ); + return fOut; +} + +vec3 PixelShaderDoLightingLinear( vec3 worldPos, vec3 worldNormal ) +{ + vec3 linearColor = vec3(0.0,0.0,0.0); + linearColor += DiffuseTerm( worldNormal, testLightDir ); + + return linearColor; +} + +vec3 LinearToGamma( vec3 f3linear ) +{ + return pow( f3linear, vec3(1.0 / 2.2) ); +} + +vec3 GammaToLinear( vec3 f3gamma ) +{ + return pow( f3gamma, vec3(2.2) ); +} + +void main() +{ + vec3 tangentSpaceNormal = vec3( 0.0, 0.0, 1.0 ); + vec4 normalTexel = vec4(1.0,1.0,1.0,1.0); + vec3 colorInput = GammaToLinear( texture( uBasetexture, lUv ).rgb ); + + vec4 baseColor = vec4( colorInput * colour.rgb, 1.0 ); + + //normalTexel = tex2D( BumpmapSampler, i.detailOrBumpTexCoord ); + //tangentSpaceNormal = 2.0 * normalTexel - 1.0; + + vec3 diffuseLighting = vec3( 1.0, 1.0, 1.0 ); + + vec3 staticLightingColor = vec3( 0.0, 0.0, 0.0 ); + diffuseLighting = PixelShaderDoLightingLinear( lPos, lNormal ); + + // multiply by .5 since we want a 50% (in gamma space) reflective surface) + diffuseLighting *= pow( 0.5, 2.2 ); + + vec3 result = diffuseLighting * baseColor.xyz; + + FragColor = vec4( LinearToGamma(result), 1.0 ); +} +""") + # Render functions # def cxr_ui(_,context): - global cxr_jobs_batch, cxr_ui_shader, cxr_jobs_inf + global cxr_jobs_batch, cxr_ui_shader, cxr_jobs_inf, cxr_error_inf w = gpu.state.viewport_get()[2] cxr_ui_shader.bind() cxr_ui_shader.uniform_float( "scale", w ) - if cxr_jobs_batch != None: + if cxr_error_inf != None: + err_begin = 50 + + if isinstance(cxr_error_inf[1],list): + err_begin += 20*(len(cxr_error_inf[1])-1) + + blf.position(0,2,err_begin,0) + blf.size(0,50,48) + blf.color(0, 1.0,0.2,0.2,0.9) + blf.draw(0,cxr_error_inf[0]) + + blf.size(0,50,24) + blf.color(0, 1.0,1.0,1.0,1.0) + + if isinstance(cxr_error_inf[1],list): + for i,inf in enumerate(cxr_error_inf[1]): + blf.position(0,2,err_begin-30-i*20,0) + blf.draw(0,inf) + else: + blf.position(0,2,err_begin-30,0) + blf.draw(0,cxr_error_inf[1]) + + elif cxr_jobs_batch != None: gpu.state.blend_set('ALPHA') cxr_jobs_batch.draw(cxr_ui_shader) @@ -89,12 +233,6 @@ def cxr_ui(_,context): blf.draw(0,ln[:-1]) py += 16 - #if CXR_PREVIEW_OPERATOR.LASTERR != None: - # blf.position(0,2,80,0) - # blf.size(0,50,48) - # blf.color(0,1.0,0.2,0.2,0.9) - # blf.draw(0,"Invalid geometry") - # Something is off with TIMER, # this forces the viewport to redraw before we can continue with our # compilation stuff. @@ -102,7 +240,8 @@ def cxr_ui(_,context): CXR_COMPILER_CHAIN.WAIT_REDRAW = False def cxr_draw(): - global cxr_view_shader, cxr_view_mesh, cxr_view_lines + global cxr_view_shader, cxr_view_mesh, cxr_view_lines, cxr_mdl_shader,\ + cxr_mdl_mesh, cxr_test_mdl cxr_view_shader.bind() @@ -115,11 +254,37 @@ def cxr_draw(): if cxr_view_lines != None: cxr_view_lines.draw( cxr_view_shader ) - gpu.state.depth_test_set('LESS_EQUAL') - gpu.state.blend_set('ADDITIVE') if cxr_view_mesh != None: + gpu.state.depth_test_set('LESS_EQUAL') + gpu.state.blend_set('ADDITIVE') + cxr_view_mesh.draw( cxr_view_shader ) + # Models + gpu.state.depth_mask_set(True) + gpu.state.depth_test_set('LESS_EQUAL') + gpu.state.face_culling_set('FRONT') + gpu.state.blend_set('NONE') + + cxr_mdl_shader.bind() + cxr_mdl_shader.uniform_float("viewProjectionMatrix", \ + bpy.context.region_data.perspective_matrix) + + if cxr_test_mdl != None: + cxr_mdl_shader.uniform_float('colour',(1.0,1.0,1.0,1.0)) + + #temp light dir + testmdl = bpy.context.scene.objects['target'] + light = bpy.context.scene.objects['point'] + relative = light.location - testmdl.location + relative.normalize() + cxr_mdl_shader.uniform_float("modelMatrix", testmdl.matrix_world) + cxr_mdl_shader.uniform_float("testLightDir", relative) + + for part in cxr_test_mdl: + cxr_mdl_shader.uniform_sampler("uBasetexture", part[0]['basetexture']) + part[1].draw( cxr_mdl_shader ) + def cxr_jobs_update_graph(jobs): global cxr_jobs_batch, cxr_ui_shader, cxr_jobs_inf @@ -186,10 +351,11 @@ def scene_redraw(): # Shared libraries # ------------------------------------------------------------------------------ -# dlclose for reloading modules manually -libc_dlclose = None -libc_dlclose = cdll.LoadLibrary(None).dlclose -libc_dlclose.argtypes = [c_void_p] +if CXR_GNU_LINUX==1: + # dlclose for reloading modules manually + libc_dlclose = None + libc_dlclose = cdll.LoadLibrary(None).dlclose + libc_dlclose.argtypes = [c_void_p] # wrapper for ctypes binding class extern(): @@ -222,7 +388,8 @@ class cxr_edge(Structure): class cxr_static_loop(Structure): _fields_ = [("index",c_int32), ("edge_index",c_int32), - ("uv",c_double * 2)] + ("uv",c_double * 2), + ("alpha",c_double)] class cxr_polygon(Structure): _fields_ = [("loop_start",c_int32), @@ -250,23 +417,58 @@ class cxr_static_mesh(Structure): class cxr_tri_mesh(Structure): _fields_ = [("vertices",POINTER(c_double *3)), + ("normals",POINTER(c_double *3)), + ("uvs",POINTER(c_double *2)), ("colours",POINTER(c_double *4)), ("indices",POINTER(c_int32)), ("indices_count",c_int32), ("vertex_count",c_int32)] +class cxr_visgroup(Structure): + _fields_ = [("name",c_char_p)] + class cxr_vmf_context(Structure): _fields_ = [("mapversion",c_int32), ("skyname",c_char_p), ("detailvbsp",c_char_p), ("detailmaterial",c_char_p), + ("visgroups",POINTER(cxr_visgroup)), + ("visgroup_count",c_int32), ("scale",c_double), ("offset",c_double *3), ("lightmap_scale",c_int32), + ("visgroupid",c_int32), ("brush_count",c_int32), ("entity_count",c_int32), ("face_count",c_int32)] +# Valve wrapper types +class fs_locator(Structure): + _fields_ = [("vpk_entry",c_void_p), + ("path",c_char_p*1024)] + +class valve_material(Structure): + _fields_ = [("basetexture",c_char_p), + ("bumpmap",c_char_p)] + +class valve_model_batch(Structure): + _fields_ = [("material",c_uint32), + ("ibstart",c_uint32), + ("ibcount",c_uint32)] + +class valve_model(Structure): + _fields_ = [("vertex_data",POINTER(c_float)), + ("indices",POINTER(c_uint32)), + ("indices_count",c_uint32), + ("vertex_count",c_uint32), + ("part_count",c_uint32), + ("material_count",c_uint32), + ("materials",POINTER(c_char_p)), + ("parts",POINTER(valve_model_batch)), + ("studiohdr",c_void_p), + ("vtxhdr",c_void_p), + ("vvdhdr",c_void_p)] + # Convert blenders mesh format into CXR's static format (they are very similar) # def mesh_cxr_format(obj): @@ -309,6 +511,14 @@ def mesh_cxr_format(obj): else: loop_data[loop_index].uv[0] = c_double(0.0) loop_data[loop_index].uv[1] = c_double(0.0) + + if data.vertex_colors: + alpha = data.vertex_colors.active.data[loop_index].color[0] + else: + alpha = 0.0 + + loop_data[loop_index].alpha = alpha + center = obj.matrix_world @ poly.center normal = mtx_rot @ poly.normal @@ -430,15 +640,36 @@ class vdf_structure(): # Other libcxr_lightpatch_bsp = extern( "cxr_lightpatch_bsp", [c_char_p], None ) +# Binary file formats and FS +libcxr_fs_set_gameinfo = extern( "cxr_fs_set_gameinfo", [c_char_p], c_int32 ) +libcxr_fs_exit = extern( "cxr_fs_exit", [], None ) +libcxr_fs_get = extern( "cxr_fs_get", [c_char_p, c_int32], c_void_p ) +libcxr_fs_free = extern( "cxr_fs_free", [c_void_p], None ) +libcxr_fs_find = extern( "cxr_fs_find", [c_char_p, POINTER(fs_locator)],\ + c_int32 ) + +libcxr_valve_load_model = extern( "valve_load_model", [c_char_p], \ + POINTER(valve_model) ) +libcxr_valve_free_model = extern( "valve_free_model", [POINTER(valve_model)],\ + None ) + +libcxr_valve_load_material = extern( "valve_load_material", [c_char_p], \ + POINTER(valve_material) ) +libcxr_valve_free_material = extern( "valve_free_material", \ + [POINTER(valve_material)], None ) + libcxr_funcs = [ libcxr_decompose, libcxr_free_world, libcxr_begin_vmf, \ libcxr_vmf_begin_entities, libcxr_push_world_vmf, \ libcxr_end_vmf, libcxr_vdf_open, libcxr_vdf_close, \ - libcxr_vdf_put, libcxr_vdf_node, libcxr_vdf_edon, + libcxr_vdf_put, libcxr_vdf_node, libcxr_vdf_edon, \ libcxr_vdf_kv, libcxr_lightpatch_bsp, libcxr_write_test_data,\ - libcxr_world_preview, libcxr_free_tri_mesh ] + libcxr_world_preview, libcxr_free_tri_mesh, \ + libcxr_fs_set_gameinfo, libcxr_fs_exit, libcxr_fs_get, \ + libcxr_fs_find, libcxr_fs_free, \ + libcxr_valve_load_model, libcxr_valve_free_model,\ + libcxr_valve_load_material, libcxr_valve_free_material ] # Callbacks - def libcxr_log_callback(logStr): print( F"{logStr.decode('utf-8')}",end='' ) @@ -466,6 +697,23 @@ def libcxr_line_callback( p0,p1,colour ): cxr_line_colours += [(colour[0],colour[1],colour[2],colour[3])] cxr_line_colours += [(colour[0],colour[1],colour[2],colour[3])] +def cxr_reset_all(): + global cxr_jobs_inf, cxr_jobs_batch, cxr_error_inf, cxr_view_mesh, \ + cxr_asset_lib + cxr_jobs_inf = None + cxr_jobs_batch = None + cxr_error_inf = None + + cxr_reset_lines() + cxr_batch_lines() + cxr_view_mesh = None + + cxr_asset_lib['models'] = {} + cxr_asset_lib['materials'] = {} + cxr_asset_lib['textures'] = {} + + scene_redraw() + # libnbvtf # ------------------------------------------------------------------------------ @@ -486,8 +734,15 @@ libnbvtf_convert = extern( "nbvtf_convert", \ [c_char_p,c_int32,c_int32,c_int32,c_int32,c_int32,c_uint32,c_char_p], \ c_int32 ) +libnbvtf_read = extern( "nbvtf_read", \ + [c_void_p,POINTER(c_int32),POINTER(c_int32), c_int32], \ + POINTER(c_uint8) ) + +libnbvtf_free = extern( "nbvtf_free", [POINTER(c_uint8)], None ) + libnbvtf_init = extern( "nbvtf_init", [], None ) -libnbvtf_funcs = [ libnbvtf_convert, libnbvtf_init ] +libnbvtf_funcs = [ libnbvtf_convert, libnbvtf_init, libnbvtf_read, \ + libnbvtf_free ] # Loading # -------------------------- @@ -497,12 +752,15 @@ def shared_reload(): # Unload libraries if existing def _reload( lib, path ): - if lib != None: - _handle = lib._handle - for i in range(10): libc_dlclose( _handle ) - lib = None - del lib - return cdll.LoadLibrary( F'{os.path.dirname(__file__)}/{path}.so' ) + if CXR_GNU_LINUX==1: + if lib != None: + _handle = lib._handle + for i in range(10): libc_dlclose( _handle ) + lib = None + del lib + + libpath = F'{os.path.dirname(__file__)}/{path}{CXR_SHARED_EXT}' + return cdll.LoadLibrary( libpath ) libnbvtf = _reload( libnbvtf, "libnbvtf" ) libcxr = _reload( libcxr, "libcxr" ) @@ -556,6 +814,16 @@ def cxr_baseclass(classes, other): base.update(x.copy()) return base +def ent_soundscape(context): + obj = context['object'] + kvs = cxr_baseclass([ent_origin],\ + { + "radius": obj.scale.x * bpy.context.scene.cxr_data.scale_factor, + "soundscape": {"type":"string","default":""} + }) + + return kvs + # EEVEE Light component converter -> Source 1 # def ent_lights(context): @@ -598,7 +866,7 @@ def ent_lights(context): elif obj.data.type == 'POINT': kvs['_light'] = [ int(x) for x in light_base] kvs['_quadratic_attn'] = 1.0 - kvs['_linear_attn'] = 0.0 + kvs['_linear_attn'] = 1.0 elif obj.data.type == 'SUN': light_base[3] *= 300.0 * 5 @@ -624,6 +892,7 @@ def ent_prop(context): kvs['origin'] = [pos[1],-pos[0],pos[2]] kvs['angles'] = [0,180,0] + kvs['uniformscale'] = 1.0 else: kvs = cxr_baseclass([ent_origin],{}) target = context['object'].instance_collection @@ -636,10 +905,15 @@ def ent_prop(context): angle[2] = euler[0] kvs['angles'] = angle + kvs['uniformscale'] = obj.scale[0] + + if target.cxr_data.shadow_caster: + kvs['enablelightbounce'] = 1 + kvs['disableshadows'] = 0 + else: + kvs['enablelightbounce'] = 0 + kvs['disableshadows'] = 1 - - kvs['enablelightbounce'] = 1 - kvs['disableshadows'] = 0 kvs['fademindist'] = -1 kvs['fadescale'] = 1 kvs['model'] = F"{asset_path('models',target)}.mdl".lower() @@ -647,7 +921,6 @@ def ent_prop(context): kvs['rendercolor'] = [255, 255, 255] kvs['skin'] = 0 kvs['solid'] = 6 - kvs['uniformscale'] = 1.0 return kvs @@ -710,7 +983,7 @@ def asset_uid(asset): name = "" if v == 0: - name = "A" + name = "a" else: dig = [] @@ -739,9 +1012,85 @@ def asset_full_path(sdir,asset): return F"{bpy.context.scene.cxr_data.subdir}/"+\ F"{asset_path(sdir,asset_uid(asset))}" +# Decomposes mesh, and sets global error information if failed. +# - returns None on fail +# - returns world on success +def cxr_decompose_globalerr( mesh_src ): + global cxr_error_inf + + err = c_int32(0) + world = libcxr_decompose.call( mesh_src, pointer(err) ) + + if not world: + cxr_view_mesh = None + cxr_batch_lines() + + cxr_error_inf = [\ + ("No Error", "There is no error?"),\ + ("Bad input", "Non manifold geometry is present in the input mesh"),\ + ("Bad result","An invalid manifold was generated, try to simplify"),\ + ("Bad result","Make sure there is a clear starting point"),\ + ("Bad result","Implicit vertex was invalid, try to simplify"),\ + ("Bad input","Non coplanar vertices are in the source mesh"),\ + ("Bad input","Non convex polygon is in the source mesh"),\ + ("Bad result","Undefined failure"),\ + ("Invalid Input", "Undefined failure"),\ + ][err.value] + + scene_redraw() + + return world + # Entity functions / infos # ------------------------ +def cxr_collection_purpose(collection): + if collection.name.startswith('.'): return None + if collection.hide_render: return None + if collection.name.startswith('mdl_'): return 'model' + return 'group' + +def cxr_object_purpose(obj): + objpurpose = None + group = None + + def _search(collection): + nonlocal objpurpose, group, obj + + purpose = cxr_collection_purpose( collection ) + if purpose == None: return + if purpose == 'model': + for o in collection.objects: + if o == obj: + if o.type != 'EMPTY': + objpurpose = 'model' + group = collection + return + return + for o in collection.objects: + if o == obj: + classname = cxr_classname(o) + if classname != None: + objpurpose = 'entity' + if o.type == 'MESH': + objpurpose = 'brush_entity' + group = collection + else: + if o.type == 'MESH': + objpurpose = 'brush' + group = collection + return + for c in collection.children: + _search(c) + + if 'main' in bpy.data.collections: + _search( bpy.data.collections['main'] ) + + if objpurpose == None and 'skybox' in bpy.data.collections: + _search( bpy.data.collections['skybox'] ) + + return (group,objpurpose) + def cxr_intrinsic_classname(obj): if obj.type == 'LIGHT': return { @@ -838,12 +1187,13 @@ def material_info(mat): def _variant_apply( val ): nonlocal mat - if isinstance( val, str ): - return val - else: + if isinstance( val, list ): for shader_variant in val: if shader_variant[0] == mat.cxr_data.shader: return shader_variant[1] + return val[0][1] + else: + return val # Find rootnodes if node == None: @@ -851,36 +1201,38 @@ def material_info(mat): for node_idname in node_def: for n in mat.node_tree.nodes: - if n.bl_idname == node_idname: + if n.name == node_idname: node_def = node_def[node_idname] node = n break for link in node_def: - if isinstance( node_def[link], dict ): - inputt = node.inputs[link] - inputt_def = node_def[link] + link_def = _variant_apply( node_def[link] ) - if inputt.is_linked: + if isinstance( link_def, dict ): + node_link = node.inputs[link] + + if node_link.is_linked: # look for definitions for the connected node type - con = inputt.links[0].from_node + from_node = node_link.links[0].from_node - for node_idname in inputt_def: - if con.bl_idname == node_idname: - con_def = inputt_def[ node_idname ] - _graph_read( con_def, con, depth+1 ) + node_name = from_node.name.split('.')[0] + if node_name in link_def: + from_node_def = link_def[ node_name ] + + _graph_read( from_node_def, from_node, depth+1 ) - # No definition found! :( + # No definition! :( # TODO: Make a warning for this? else: - if "default" in inputt_def: - prop = _variant_apply( inputt_def['default'] ) - info[prop] = inputt.default_value + if "default" in link_def: + prop = _variant_apply( link_def['default'] ) + info[prop] = node_link.default_value else: - prop = _variant_apply( node_def[link] ) - info[prop] = getattr(node,link) + prop = _variant_apply( link_def ) + info[prop] = getattr( node, link ) _graph_read(cxr_graph_mapping) @@ -947,11 +1299,10 @@ def cxr_scene_collect(): def _collect(collection,transform): nonlocal sceneinfo - - if collection.name.startswith('.'): return - if collection.hide_render: return - - if collection.name.startswith('mdl_'): + + purpose = cxr_collection_purpose( collection ) + if purpose == None: return + if purpose == 'model': sceneinfo['entities'] += [{ "object": collection, "classname": "prop_static", @@ -1031,6 +1382,12 @@ def cxr_export_vmf(sceneinfo, output_vmf): vmfinfo.entity_count = 0 vmfinfo.face_count = 0 + visgroups = (cxr_visgroup*len(cxr_visgroups))() + for i, vg in enumerate(cxr_visgroups): + visgroups[i].name = vg.encode('utf-8') + vmfinfo.visgroups = cast(visgroups, POINTER(cxr_visgroup)) + vmfinfo.visgroup_count = len(cxr_visgroups) + libcxr_begin_vmf.call( pointer(vmfinfo), m.fp ) def _buildsolid( cmd ): @@ -1039,7 +1396,7 @@ def cxr_export_vmf(sceneinfo, output_vmf): print( F"{vmfinfo.brush_count} :: {cmd['object'].name}" ) baked = mesh_cxr_format( cmd['object'] ) - world = libcxr_decompose.call( baked, None ) + world = cxr_decompose_globalerr( baked ) if world == None: return False @@ -1051,6 +1408,11 @@ def cxr_export_vmf(sceneinfo, output_vmf): vmfinfo.offset[1] = offset[1] vmfinfo.offset[2] = offset[2] + if cmd['object'].cxr_data.lightmap_override > 0: + vmfinfo.lightmap_scale = cmd['object'].cxr_data.lightmap_override + else: + vmfinfo.lightmap_scale = bpy.context.scene.cxr_data.lightmap_scale + libcxr_push_world_vmf.call( world, pointer(vmfinfo), m.fp ) libcxr_free_world.call( world ) @@ -1058,10 +1420,12 @@ def cxr_export_vmf(sceneinfo, output_vmf): # 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) @@ -1085,12 +1449,21 @@ def cxr_export_vmf(sceneinfo, output_vmf): 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 @@ -1237,6 +1610,12 @@ def compile_material(mat): 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() @@ -1340,8 +1719,16 @@ def cxr_export_modelsrc( mdl, origin, asset_dir, project_name, transform ): 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') + 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") @@ -1372,6 +1759,15 @@ class CXR_RELOAD(bpy.types.Operator): 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): @@ -1384,65 +1780,179 @@ class CXR_DEV_OPERATOR(bpy.types.Operator): 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" - 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'} + global cxr_view_shader, cxr_view_mesh, cxr_error_inf + + cxr_reset_all() - 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) ) + world = cxr_decompose_globalerr( mesh_src ) 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'} + return {'FINISHED'} # Generate preview using cxr # @@ -1469,15 +1979,8 @@ class CXR_PREVIEW_OPERATOR(bpy.types.Operator): 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'} + + return {'FINISHED'} # Search for VMF compiler executables in subdirectory # @@ -1497,6 +2000,30 @@ class CXR_DETECT_COMPILERS(bpy.types.Operator): 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): @@ -1517,7 +2044,7 @@ class CXR_COMPILER_CHAIN(bpy.types.Operator): JOBSYS = None def cancel(_,context): - global cxr_jobs_batch + #global cxr_jobs_batch static = _.__class__ wm = context.window_manager @@ -1531,7 +2058,7 @@ class CXR_COMPILER_CHAIN(bpy.types.Operator): static.FILE.close() - cxr_jobs_batch = None + #cxr_jobs_batch = None scene_redraw() return {'FINISHED'} @@ -1539,7 +2066,7 @@ class CXR_COMPILER_CHAIN(bpy.types.Operator): static = _.__class__ if ev.type == 'TIMER': - global cxr_jobs_batch + global cxr_jobs_batch, cxr_error_inf if static.WAIT_REDRAW: scene_redraw() @@ -1553,15 +2080,16 @@ class CXR_COMPILER_CHAIN(bpy.types.Operator): 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() + # 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(cxr_temp_file("convexer_compile_log.txt"),"r") as log: + static.LOG = log.readlines() + if status == None: return {'PASS_THROUGH'} else: #for l in static.SUBPROC.stdout: @@ -1570,6 +2098,10 @@ class CXR_COMPILER_CHAIN(bpy.types.Operator): if status != 0: print(F'Compiler () error: {status}') + + jobn = static.JOBSYS['jobs'][static.JOBID] + cxr_error_inf = ( F"{static.JOBSYS['title']} error {status}", jobn ) + return _.cancel(context) static.JOBSYS['jobs'][static.JOBID] = None @@ -1606,206 +2138,273 @@ class CXR_COMPILER_CHAIN(bpy.types.Operator): # All completed print( "All jobs completed!" ) - cxr_jobs_batch = None - - scene_redraw() + #cxr_jobs_batch = None + #scene_redraw() return _.cancel(context) return {'PASS_THROUGH'} def invoke(_,context,event): + global cxr_error_inf + static = _.__class__ wm = context.window_manager - if static.TIMER == None: - print("Launching compiler toolchain") + if static.TIMER != None: + print("Chain exiting...") + static.USER_EXIT=True + return {'RUNNING_MODAL'} - # 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 = [] + print("Launching compiler toolchain") + cxr_reset_all() + + # 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" + + bsp_local = F"{directory}/{settings.project_name}.bsp" + bsp_remote = F"{settings.subdir}/maps/{settings.project_name}.bsp" + bsp_packed = F"{settings.subdir}/maps/{settings.project_name}_pack.bsp" + packlist = F"{directory}/{settings.project_name}_assets.txt" + + os.makedirs( asset_dir, exist_ok=True ) + os.makedirs( material_dir, exist_ok=True ) + os.makedirs( model_dir, exist_ok=True ) + + static.FILE = open(cxr_temp_file("convexer_compile_log.txt"),"w") + static.LOG = [] - sceneinfo = cxr_scene_collect() - image_jobs = [] - qc_jobs = [] + 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 + + cxr_error_inf = ( "Shader error", \ + F"Vertex shader ({errmat}) used on model ({errnam})" ) + + print( F"Vertex shader {errmat} used on {errnam}") + scene_redraw() + 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'] - # Collect materials - a_materials = set() - for brush in sceneinfo['geo']: - for ms in brush['object'].material_slots: + for obj in mdl.objects: + for ms in obj.material_slots: a_materials.add( ms.material ) - if ms.material.cxr_data.shader == 'VertexLitGeneric': + if ms.material.cxr_data.shader == 'LightMappedGeneric' or \ + ms.material.cxr_data.shader == 'WorldVertexTransition': + errmat = ms.material.name - errnam = brush['object'].name - print( F"Vertex shader {errmat} used on {errnam}") + errnam = obj.name + + cxr_error_inf = ( "Shader error", \ + F"Lightmapped shader ({errmat}) used on model ({errnam})" ) + + print( F"Lightmapped shader {errmat} used on {errnam}") + scene_redraw() 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 + + # 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 + + # Create packlist + with open( packlist, "w" ) as fp: + 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", + if mat.cxr_data.shader == 'Builtin': continue + fp.write(F"{asset_path('materials',mat)}.vmt\n") + fp.write(F"{cxr_winepath(asset_full_path('materials',mat))}.vmt\n") + + for img_job in image_jobs: + img = img_job[0] + fp.write(F"{asset_path('materials',img)}.vtf\n") + fp.write(F"{cxr_winepath(asset_full_path('materials',img))}.vtf\n") + + for mdl in a_models: + local = asset_path('models',mdl) + winep = cxr_winepath(asset_full_path('models',mdl)) + + fp.write(F"{local}.vvd\n") + fp.write(F"{winep}.vvd\n") + fp.write(F"{local}.dx90.vtx\n") + fp.write(F"{winep}.dx90.vtx\n") + fp.write(F"{local}.mdl\n") + fp.write(F"{winep}.mdl\n") + fp.write(F"{local}.vvd\n") + fp.write(F"{winep}.vvd\n") + + if cxr_modelsrc_vphys(mdl): + fp.write(F"{local}.phy\n") + fp.write(F"{winep}.phy\n") + + # Convexer jobs + static.JOBID = 0 + static.JOBINFO = [] + + if settings.comp_vmf: + static.JOBINFO += [{ + "title": "Convexer", + "w": 20, + "colour": (0.863, 0.078, 0.235,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": (1.000, 0.271, 0.000,1.0), + "exec": compile_image, + "jobs": image_jobs + }] + + game = cxr_winepath( settings.subdir ) + args = [ \ + '-game', game, settings.project_name + ] + + # FBX stage + if settings.comp_models: + if len(model_jobs) > 0: + static.JOBINFO += [{ + "title": "Batches", + "w": 25, + "colour": (1.000, 0.647, 0.000,1.0), + "exec": cxr_export_modelsrc, + "jobs": model_jobs + }] + + if len(qc_jobs) > 0: + static.JOBINFO += [{ + "title": "StudioMDL", "w": 20, - "colour": (1.0,0.3,0.1,1.0), - "exec": cxr_export_vmf, - "jobs": [(sceneinfo,output_vmf)] + "colour": (1.000, 0.843, 0.000, 1.0), + "exec": "studiomdl", + "jobs": [[settings[F'exe_studiomdl']] + [\ + '-nop4', '-game', game, qc] for qc in qc_jobs], + "cwd": asset_dir }] - - 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: + + # VBSP stage + if settings.comp_compile: + if not settings.opt_vbsp.startswith( 'disable' ): + vbsp_opt = settings.opt_vbsp.split() static.JOBINFO += [{ "title": "VBSP", "w": 25, - "colour": (0.1,0.2,1.0,1.0), + "colour": (0.678, 1.000, 0.184,1.0), "exec": "vbsp", - "jobs": [[settings[F'exe_vbsp']] + args], + "jobs": [[settings[F'exe_vbsp']] + vbsp_opt + args], "cwd": directory }] - + + if not settings.opt_vvis.startswith( 'disable' ): + vvis_opt = settings.opt_vvis.split() static.JOBINFO += [{ "title": "VVIS", "w": 25, - "colour": (0.9,0.5,0.5,1.0), + "colour": (0.000, 1.000, 0.498,1.0), "exec": "vvis", - "jobs": [[settings[F'exe_vvis']] + ['-fast'] + args ], + "jobs": [[settings[F'exe_vvis']] + vvis_opt + args ], "cwd": directory }] - + + if not settings.opt_vrad.startswith( 'disable' ): vrad_opt = settings.opt_vrad.split() static.JOBINFO += [{ "title": "VRAD", "w": 25, - "colour": (0.9,0.2,0.3,1.0), + "colour": (0.125, 0.698, 0.667,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.JOBINFO += [{ + "title": "CXR", + "w": 5, + "colour": (0.118, 0.565, 1.000,1.0), + "exec": cxr_patchmap, + "jobs": [(bsp_local,bsp_remote)] + }] - static.USER_EXIT=False - static.TIMER=wm.event_timer_add(0.1,window=context.window) - wm.modal_handler_add(_) + if settings.comp_pack: + static.JOBINFO += [{ + "title": "Pack", + "w": 5, + "colour": (0.541, 0.169, 0.886,1.0), + "exec": "bspzip", + "jobs": [[cxr_compiler_path("bspzip"), '-addlist', \ + cxr_winepath(bsp_remote), + cxr_winepath(packlist), + cxr_winepath(bsp_packed) ]], + "cwd": directory + }] - cxr_jobs_update_graph( static.JOBINFO ) - scene_redraw() - return {'RUNNING_MODAL'} + if len(static.JOBINFO) == 0: + return {'CANCELLED'} + + static.USER_EXIT=False + static.TIMER=wm.event_timer_add(0.1,window=context.window) + wm.modal_handler_add(_) - print("Chain exiting...") - static.USER_EXIT=True + cxr_jobs_update_graph( static.JOBINFO ) + scene_redraw() return {'RUNNING_MODAL'} class CXR_RESET_HASHES(bpy.types.Operator): @@ -1869,19 +2468,37 @@ class CXR_VIEW3D( bpy.types.Panel ): bl_region_type = 'UI' bl_category = "Convexer" - @classmethod - def poll(cls, context): - return (context.object is not None) - def draw(_, context): layout = _.layout + + active_object = context.object + if active_object == None: return + + purpose = cxr_object_purpose( active_object ) + + if purpose[0] == None or purpose[1] == None: + usage_str = "No purpose" + else: + if purpose[1] == 'model': + usage_str = F'mesh in {asset_name( purpose[0] )}.mdl' + else: + usage_str = F'{purpose[1]} in {purpose[0].name}' + + layout.label(text=F"Currently editing:") + box = layout.box() + box.label(text=usage_str) + + if purpose[1] == 'brush' or purpose[1] == 'brush_entity': + row = layout.row() + row.scale_y = 2 + row.operator("convexer.preview") + row = layout.row() row.scale_y = 2 - row.operator("convexer.preview") + row.operator("convexer.reset") - if CXR_PREVIEW_OPERATOR.LASTERR != None: - box = layout.box() - box.label(text=CXR_PREVIEW_OPERATOR.LASTERR, icon='ERROR') + layout.prop( bpy.context.scene.cxr_data, "dev_mdl" ) + layout.operator( "convexer.model_load" ) # Main scene properties interface, where all the settings go # @@ -1893,14 +2510,14 @@ class CXR_INTERFACE(bpy.types.Panel): bl_context="scene" def draw(_,context): - _.layout.operator("convexer.reload") - _.layout.operator("convexer.dev_test") - _.layout.operator("convexer.preview") - _.layout.operator("convexer.hash_reset") + if CXR_GNU_LINUX==1: + _.layout.operator("convexer.reload") + _.layout.operator("convexer.dev_test") + _.layout.operator("convexer.fs_init") + _.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" ) @@ -1917,7 +2534,11 @@ class CXR_INTERFACE(bpy.types.Panel): box.operator("convexer.detect_compilers") box.prop(settings, "exe_studiomdl") box.prop(settings, "exe_vbsp") + box.prop(settings, "opt_vbsp") + box.prop(settings, "exe_vvis") + box.prop(settings, "opt_vvis") + box.prop(settings, "exe_vrad") box.prop(settings, "opt_vrad") @@ -1927,12 +2548,18 @@ class CXR_INTERFACE(bpy.types.Panel): row.prop(settings,"comp_textures") row.prop(settings,"comp_models") row.prop(settings,"comp_compile") + row.prop(settings,"comp_pack") text = "Compile" if CXR_COMPILER_CHAIN.TIMER == None else "Cancel" row = box.row() row.scale_y = 3 row.operator("convexer.chain", text=text) + row = box.row() + row.scale_y = 2 + row.operator("convexer.reset") + if CXR_COMPILER_CHAIN.TIMER != None: + row.enabled = False class CXR_MATERIAL_PANEL(bpy.types.Panel): bl_label="VMT Properties" @@ -2050,7 +2677,7 @@ def cxr_entity_changeclass(_,context): entdef = cxr_entities[classname] kvs = entdef['keyvalues'] - if callable(kvs): kvs = kvs(active_object) + if callable(kvs): kvs = kvs( {'object': active_object} ) for k in kvs: kv = kvs[k] @@ -2088,6 +2715,9 @@ class CXR_ENTITY_PANEL(bpy.types.Panel): _.layout.prop( active_object.cxr_data, 'brushclass' ) else: _.layout.prop( active_object.cxr_data, 'classname' ) + _.layout.prop( active_object.cxr_data, 'visgroup' ) + _.layout.prop( active_object.cxr_data, 'lightmap_override' ) + if classname == 'NONE': return else: @@ -2136,6 +2766,26 @@ class CXR_LIGHT_PANEL(bpy.types.Panel): elif active_object.type == 'LIGHT_PROBE': layout.prop( properties, "size" ) +class CXR_COLLECTION_PANEL(bpy.types.Panel): + bl_label = "Source Settings" + bl_idname = "COL_PT_cxr" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "collection" + + def draw(self, context): + layout = self.layout + scene = context.scene + + active_collection = bpy.context.collection + + if active_collection != None: + layout.prop( active_collection.cxr_data, "shadow_caster" ) + layout.prop( active_collection.cxr_data, "texture_shadows" ) + layout.prop( active_collection.cxr_data, "preserve_order" ) + layout.prop( active_collection.cxr_data, "surfaceprop" ) + layout.prop( active_collection.cxr_data, "visgroup" ) + # Settings groups # ------------------------------------------------------------------------------ @@ -2205,14 +2855,29 @@ class CXR_ENTITY_SETTINGS(bpy.types.PropertyGroup): brushclass: bpy.props.EnumProperty(items=enum_brushents, name="Class", \ update=cxr_entity_changeclass, default='NONE' ) + + enum_classes = [('0',"None","")] + for i, vg in enumerate(cxr_visgroups): + enum_classes += [(str(i+1),vg,"")] + visgroup: bpy.props.EnumProperty(name="visgroup",items=enum_classes,default=0) + lightmap_override: bpy.props.IntProperty(name="Lightmap Override",default=0) class CXR_MODEL_SETTINGS(bpy.types.PropertyGroup): last_hash: bpy.props.StringProperty( name="" ) asset_id: bpy.props.IntProperty(name="vmf_settings",default=0) + shadow_caster: bpy.props.BoolProperty( name="Shadow caster", default=True ) + texture_shadows: bpy.props.BoolProperty( name="Texture Shadows", default=False ) + preserve_order: bpy.props.BoolProperty( name="Preserve Order", default=False ) + surfaceprop: bpy.props.StringProperty( name="Suface prop",default="default" ) + + enum_classes = [('0',"None","")] + for i, vg in enumerate(cxr_visgroups): + enum_classes += [(str(i+1),vg,"")] + visgroup: bpy.props.EnumProperty(name="visgroup",items=enum_classes,default=0) class CXR_SCENE_SETTINGS(bpy.types.PropertyGroup): project_name: bpy.props.StringProperty( name="Project Name" ) - subdir: bpy.props.StringProperty( name="Subdirectory" ) + subdir: bpy.props.StringProperty( name="../csgo/ folder" ) exe_studiomdl: bpy.props.StringProperty( name="studiomdl" ) exe_vbsp: bpy.props.StringProperty( name="vbsp" ) @@ -2223,7 +2888,6 @@ class CXR_SCENE_SETTINGS(bpy.types.PropertyGroup): opt_vrad: bpy.props.StringProperty( name="args", \ default="-reflectivityScale 0.35 -aoscale 1.4 -final -textureshadows -hdr -StaticPropLighting -StaticPropPolys" ) - debug: bpy.props.BoolProperty(name="Debug",default=False) scale_factor: bpy.props.FloatProperty( name="VMF Scale factor", \ default=32.0,min=1.0) skybox_scale_factor: bpy.props.FloatProperty( name="Sky Scale factor", \ @@ -2242,6 +2906,9 @@ class CXR_SCENE_SETTINGS(bpy.types.PropertyGroup): comp_models: bpy.props.BoolProperty(name="Models",default=True) comp_textures: bpy.props.BoolProperty(name="Textures",default=True) comp_compile: bpy.props.BoolProperty(name="Compile",default=True) + comp_pack: bpy.props.BoolProperty(name="Pack",default=False) + + dev_mdl: bpy.props.StringProperty(name="Model",default="") classes = [ CXR_RELOAD, CXR_DEV_OPERATOR, CXR_INTERFACE, \ CXR_MATERIAL_PANEL, CXR_IMAGE_SETTINGS,\ @@ -2249,7 +2916,8 @@ classes = [ CXR_RELOAD, CXR_DEV_OPERATOR, CXR_INTERFACE, \ CXR_LIGHT_SETTINGS, CXR_SCENE_SETTINGS, CXR_DETECT_COMPILERS,\ CXR_ENTITY_PANEL, CXR_LIGHT_PANEL, CXR_PREVIEW_OPERATOR,\ CXR_VIEW3D, CXR_COMPILER_CHAIN, CXR_RESET_HASHES,\ - CXR_COMPILE_MATERIAL] + CXR_COMPILE_MATERIAL, CXR_COLLECTION_PANEL, CXR_RESET, \ + CXR_INIT_FS_OPERATOR, CXR_LOAD_MODEL_OPERATOR ] vmt_param_dynamic_class = None