refactor config for wvt
[convexer.git] / __init__.py
1 # CONVEXER v0.95
2 #
3 # A GNU/Linux-first Source1 Hammer replacement
4 # built with Blender, for mapmakers
5 #
6 # Copyright (C) 2022 Harry Godden (hgn)
7 #
8 # LICENSE: GPLv3.0, please see COPYING and LICENSE for more information
9 #
10
11 bl_info = {
12 "name":"Convexer",
13 "author": "Harry Godden (hgn)",
14 "version": (0,1),
15 "blender":(3,1,0),
16 "location":"Export",
17 "descriptin":"",
18 "warning":"",
19 "wiki_url":"",
20 "category":"Import/Export",
21 }
22
23 print( "Convexer reload" )
24
25 #from mathutils import *
26 import bpy, gpu, math, os, time, mathutils, blf, subprocess, shutil, hashlib
27 from ctypes import *
28 from gpu_extras.batch import batch_for_shader
29 from bpy.app.handlers import persistent
30
31 # Setup platform dependent variables
32
33 exec(open(F'{os.path.dirname(__file__)}/platform.py').read())
34 if CXR_GNU_LINUX==1:
35 CXR_SHARED_EXT=".so"
36 CXR_EXE_EXT=""
37 else:
38 CXR_SHARED_EXT=".dll"
39 CXR_EXE_EXT=".exe"
40
41 # GPU and viewport drawing
42 # ------------------------------------------------------------------------------
43
44 # Handlers
45 cxr_view_draw_handler = None
46 cxr_ui_draw_handler = None
47
48 # Batches
49 cxr_view_lines = None
50 cxr_view_mesh = None
51 cxr_jobs_batch = None
52 cxr_jobs_inf = []
53 cxr_error_inf = None
54
55 # Shaders
56 cxr_view_shader = gpu.shader.from_builtin('3D_SMOOTH_COLOR')
57 cxr_ui_shader = gpu.types.GPUShader("""
58 uniform mat4 ModelViewProjectionMatrix;
59 uniform float scale;
60
61 in vec2 aPos;
62 in vec4 aColour;
63
64 out vec4 colour;
65
66 void main()
67 {
68 gl_Position = ModelViewProjectionMatrix * vec4(aPos.x*scale,aPos.y, 0.0, 1.0);
69 colour = aColour;
70 }
71 ""","""
72 in vec4 colour;
73 out vec4 FragColor;
74
75 void main()
76 {
77 FragColor = colour;
78 }
79 """)
80
81 # Render functions
82 #
83 def cxr_ui(_,context):
84 global cxr_jobs_batch, cxr_ui_shader, cxr_jobs_inf, cxr_error_inf
85
86 w = gpu.state.viewport_get()[2]
87 cxr_ui_shader.bind()
88 cxr_ui_shader.uniform_float( "scale", w )
89
90 if cxr_error_inf != None:
91 err_begin = 50
92
93 if isinstance(cxr_error_inf[1],list):
94 err_begin += 20*(len(cxr_error_inf[1])-1)
95
96 blf.position(0,2,err_begin,0)
97 blf.size(0,50,48)
98 blf.color(0, 1.0,0.2,0.2,0.9)
99 blf.draw(0,cxr_error_inf[0])
100
101 blf.size(0,50,24)
102 blf.color(0, 1.0,1.0,1.0,1.0)
103
104 if isinstance(cxr_error_inf[1],list):
105 for i,inf in enumerate(cxr_error_inf[1]):
106 blf.position(0,2,err_begin-30-i*20,0)
107 blf.draw(0,inf)
108 else:
109 blf.position(0,2,err_begin-30,0)
110 blf.draw(0,cxr_error_inf[1])
111
112 elif cxr_jobs_batch != None:
113 gpu.state.blend_set('ALPHA')
114 cxr_jobs_batch.draw(cxr_ui_shader)
115
116 blf.position(0,2,50,0)
117 blf.size(0,50,48)
118 blf.color(0,1.0,1.0,1.0,1.0)
119 blf.draw(0,"Compiling")
120
121 for ji in cxr_jobs_inf:
122 blf.position(0,ji[0]*w,35,0)
123 blf.size(0,50,20)
124 blf.draw(0,ji[1])
125
126 py = 80
127 blf.size(0,50,16)
128 for ln in reversed(CXR_COMPILER_CHAIN.LOG[-25:]):
129 blf.position(0,2,py,0)
130 blf.draw(0,ln[:-1])
131 py += 16
132
133 # Something is off with TIMER,
134 # this forces the viewport to redraw before we can continue with our
135 # compilation stuff.
136
137 CXR_COMPILER_CHAIN.WAIT_REDRAW = False
138
139 def cxr_draw():
140 global cxr_view_shader, cxr_view_mesh, cxr_view_lines
141
142 cxr_view_shader.bind()
143
144 gpu.state.depth_mask_set(False)
145 gpu.state.line_width_set(1.5)
146 gpu.state.face_culling_set('BACK')
147 gpu.state.depth_test_set('NONE')
148 gpu.state.blend_set('ALPHA')
149
150 if cxr_view_lines != None:
151 cxr_view_lines.draw( cxr_view_shader )
152
153 gpu.state.depth_test_set('LESS_EQUAL')
154 gpu.state.blend_set('ADDITIVE')
155 if cxr_view_mesh != None:
156 cxr_view_mesh.draw( cxr_view_shader )
157
158 def cxr_jobs_update_graph(jobs):
159 global cxr_jobs_batch, cxr_ui_shader, cxr_jobs_inf
160
161 cxr_jobs_inf = []
162
163 total_width = 0
164 verts = []
165 colours = []
166 indices = []
167
168 for sys in jobs:
169 total_width += sys['w']
170
171 sf = 1.0/total_width
172 cur = 0.0
173 ci = 0
174
175 for sys in jobs:
176 w = sys['w']
177 h = 30.0
178 colour = sys['colour']
179 colourwait = (colour[0],colour[1],colour[2],0.4)
180 colourrun = (colour[0]*1.5,colour[1]*1.5,colour[2]*1.5,0.5)
181 colourdone = (colour[0],colour[1],colour[2],1.0)
182
183 jobs = sys['jobs']
184 sfsub = (1.0/(len(jobs)))*w
185 i = 0
186
187 for j in jobs:
188 if j == None: colour = colourdone
189 else: colour = colourwait
190
191 px = (cur + (i)*sfsub) * sf
192 px1 = (cur + (i+1.0)*sfsub) * sf
193 i += 1
194
195 verts += [(px,0), (px, h), (px1, 0.0), (px1,h)]
196 colours += [colour,colour,colour,colour]
197 indices += [(ci+0,ci+2,ci+3),(ci+0,ci+3,ci+1)]
198 ci += 4
199
200 cxr_jobs_inf += [((sf*cur), sys['title'])]
201 cur += w
202
203 cxr_jobs_batch = batch_for_shader(
204 cxr_ui_shader, 'TRIS',
205 { "aPos": verts, "aColour": colours },
206 indices = indices
207 )
208
209 # view_layer.update() doesnt seem to work,
210 # tag_redraw() seems to have broken
211 # therefore, change a property
212 def scene_redraw():
213 ob = bpy.context.scene.objects[0]
214 ob.hide_render = ob.hide_render
215
216 # the 'real' way to refresh the scene
217 for area in bpy.context.window.screen.areas:
218 if area.type == 'view_3d':
219 area.tag_redraw()
220
221 # Shared libraries
222 # ------------------------------------------------------------------------------
223
224 if CXR_GNU_LINUX==1:
225 # dlclose for reloading modules manually
226 libc_dlclose = None
227 libc_dlclose = cdll.LoadLibrary(None).dlclose
228 libc_dlclose.argtypes = [c_void_p]
229
230 # wrapper for ctypes binding
231 class extern():
232 def __init__(_,name,argtypes,restype):
233 _.name = name
234 _.argtypes = argtypes
235 _.restype = restype
236 _.call = None
237
238 def loadfrom(_,so):
239 _.call = getattr(so,_.name)
240 _.call.argtypes = _.argtypes
241
242 if _.restype != None:
243 _.call.restype = _.restype
244
245 # libcxr (convexer)
246 # ------------------------------------------------------------------------------
247
248 libcxr = None
249
250 # Structure definitions
251 #
252 class cxr_edge(Structure):
253 _fields_ = [("i0",c_int32),
254 ("i1",c_int32),
255 ("freestyle",c_int32),
256 ("sharp",c_int32)]
257
258 class cxr_static_loop(Structure):
259 _fields_ = [("index",c_int32),
260 ("edge_index",c_int32),
261 ("uv",c_double * 2)]
262
263 class cxr_polygon(Structure):
264 _fields_ = [("loop_start",c_int32),
265 ("loop_total",c_int32),
266 ("normal",c_double * 3),
267 ("center",c_double * 3),
268 ("material_id",c_int32)]
269
270 class cxr_material(Structure):
271 _fields_ = [("res",c_int32 * 2),
272 ("name",c_char_p)]
273
274 class cxr_static_mesh(Structure):
275 _fields_ = [("vertices",POINTER(c_double * 3)),
276 ("edges",POINTER(cxr_edge)),
277 ("loops",POINTER(cxr_static_loop)),
278 ("polys",POINTER(cxr_polygon)),
279 ("materials",POINTER(cxr_material)),
280
281 ("poly_count",c_int32),
282 ("vertex_count",c_int32),
283 ("edge_count",c_int32),
284 ("loop_count",c_int32),
285 ("material_count",c_int32)]
286
287 class cxr_tri_mesh(Structure):
288 _fields_ = [("vertices",POINTER(c_double *3)),
289 ("colours",POINTER(c_double *4)),
290 ("indices",POINTER(c_int32)),
291 ("indices_count",c_int32),
292 ("vertex_count",c_int32)]
293
294 class cxr_visgroup(Structure):
295 _fields_ = [("name",c_char_p)]
296
297 class cxr_vmf_context(Structure):
298 _fields_ = [("mapversion",c_int32),
299 ("skyname",c_char_p),
300 ("detailvbsp",c_char_p),
301 ("detailmaterial",c_char_p),
302 ("visgroups",POINTER(cxr_visgroup)),
303 ("visgroup_count",c_int32),
304 ("scale",c_double),
305 ("offset",c_double *3),
306 ("lightmap_scale",c_int32),
307 ("visgroupid",c_int32),
308 ("brush_count",c_int32),
309 ("entity_count",c_int32),
310 ("face_count",c_int32)]
311
312 # Convert blenders mesh format into CXR's static format (they are very similar)
313 #
314 def mesh_cxr_format(obj):
315 orig_state = None
316
317 if bpy.context.active_object != None:
318 orig_state = obj.mode
319 if orig_state != 'OBJECT':
320 bpy.ops.object.mode_set(mode='OBJECT')
321
322 dgraph = bpy.context.evaluated_depsgraph_get()
323 data = obj.evaluated_get(dgraph).data
324
325 _,mtx_rot,_ = obj.matrix_world.decompose()
326
327 mesh = cxr_static_mesh()
328
329 vertex_data = ((c_double*3)*len(data.vertices))()
330 for i, vert in enumerate(data.vertices):
331 v = obj.matrix_world @ vert.co
332 vertex_data[i][0] = c_double(v[0])
333 vertex_data[i][1] = c_double(v[1])
334 vertex_data[i][2] = c_double(v[2])
335
336 loop_data = (cxr_static_loop*len(data.loops))()
337 polygon_data = (cxr_polygon*len(data.polygons))()
338
339 for i, poly in enumerate(data.polygons):
340 loop_start = poly.loop_start
341 loop_end = poly.loop_start + poly.loop_total
342 for loop_index in range(loop_start, loop_end):
343 loop = data.loops[loop_index]
344 loop_data[loop_index].index = loop.vertex_index
345 loop_data[loop_index].edge_index = loop.edge_index
346
347 if data.uv_layers:
348 uv = data.uv_layers.active.data[loop_index].uv
349 loop_data[loop_index].uv[0] = c_double(uv[0])
350 loop_data[loop_index].uv[1] = c_double(uv[1])
351 else:
352 loop_data[loop_index].uv[0] = c_double(0.0)
353 loop_data[loop_index].uv[1] = c_double(0.0)
354 center = obj.matrix_world @ poly.center
355 normal = mtx_rot @ poly.normal
356
357 polygon_data[i].loop_start = poly.loop_start
358 polygon_data[i].loop_total = poly.loop_total
359 polygon_data[i].normal[0] = normal[0]
360 polygon_data[i].normal[1] = normal[1]
361 polygon_data[i].normal[2] = normal[2]
362 polygon_data[i].center[0] = center[0]
363 polygon_data[i].center[1] = center[1]
364 polygon_data[i].center[2] = center[2]
365 polygon_data[i].material_id = poly.material_index
366
367 edge_data = (cxr_edge*len(data.edges))()
368
369 for i, edge in enumerate(data.edges):
370 edge_data[i].i0 = edge.vertices[0]
371 edge_data[i].i1 = edge.vertices[1]
372 edge_data[i].freestyle = edge.use_freestyle_mark
373 edge_data[i].sharp = edge.use_edge_sharp
374
375 material_data = (cxr_material*len(obj.material_slots))()
376
377 for i, ms in enumerate(obj.material_slots):
378 inf = material_info(ms.material)
379 material_data[i].res[0] = inf['res'][0]
380 material_data[i].res[1] = inf['res'][1]
381 material_data[i].name = inf['name'].encode('utf-8')
382
383 mesh.edges = cast(edge_data, POINTER(cxr_edge))
384 mesh.vertices = cast(vertex_data, POINTER(c_double*3))
385 mesh.loops = cast(loop_data,POINTER(cxr_static_loop))
386 mesh.polys = cast(polygon_data, POINTER(cxr_polygon))
387 mesh.materials = cast(material_data, POINTER(cxr_material))
388
389 mesh.poly_count = len(data.polygons)
390 mesh.vertex_count = len(data.vertices)
391 mesh.edge_count = len(data.edges)
392 mesh.loop_count = len(data.loops)
393 mesh.material_count = len(obj.material_slots)
394
395 if orig_state != None:
396 bpy.ops.object.mode_set(mode=orig_state)
397
398 return mesh
399
400 # Callback ctypes indirection things.. not really sure.
401 c_libcxr_log_callback = None
402 c_libcxr_line_callback = None
403
404 # Public API
405 # -------------------------------------------------------------
406 libcxr_decompose = extern( "cxr_decompose",
407 [POINTER(cxr_static_mesh), POINTER(c_int32)],
408 c_void_p
409 )
410 libcxr_free_world = extern( "cxr_free_world",
411 [c_void_p],
412 None
413 )
414 libcxr_write_test_data = extern( "cxr_write_test_data",
415 [POINTER(cxr_static_mesh)],
416 None
417 )
418 libcxr_world_preview = extern( "cxr_world_preview",
419 [c_void_p],
420 POINTER(cxr_tri_mesh)
421 )
422 libcxr_free_tri_mesh = extern( "cxr_free_tri_mesh",
423 [c_void_p],
424 None
425 )
426 libcxr_begin_vmf = extern( "cxr_begin_vmf",
427 [POINTER(cxr_vmf_context), c_void_p],
428 None
429 )
430 libcxr_vmf_begin_entities = extern( "cxr_vmf_begin_entities",
431 [POINTER(cxr_vmf_context), c_void_p],
432 None
433 )
434 libcxr_push_world_vmf = extern("cxr_push_world_vmf",
435 [c_void_p,POINTER(cxr_vmf_context),c_void_p],
436 None
437 )
438 libcxr_end_vmf = extern( "cxr_end_vmf",
439 [POINTER(cxr_vmf_context),c_void_p],
440 None
441 )
442
443 # VDF + with open wrapper
444 libcxr_vdf_open = extern( "cxr_vdf_open", [c_char_p], c_void_p )
445 libcxr_vdf_close = extern( "cxr_vdf_close", [c_void_p], None )
446 libcxr_vdf_put = extern( "cxr_vdf_put", [c_void_p,c_char_p], None )
447 libcxr_vdf_node = extern( "cxr_vdf_node", [c_void_p,c_char_p], None )
448 libcxr_vdf_edon = extern( "cxr_vdf_edon", [c_void_p], None )
449 libcxr_vdf_kv = extern( "cxr_vdf_kv", [c_void_p,c_char_p,c_char_p], None )
450
451 class vdf_structure():
452 def __init__(_,path):
453 _.path = path
454 def __enter__(_):
455 _.fp = libcxr_vdf_open.call( _.path.encode('utf-8') )
456 if _.fp == None:
457 print( F"Could not open file {_.path}" )
458 return None
459 return _
460 def __exit__(_,type,value,traceback):
461 if _.fp != None:
462 libcxr_vdf_close.call(_.fp)
463 def put(_,s):
464 libcxr_vdf_put.call(_.fp, s.encode('utf-8') )
465 def node(_,name):
466 libcxr_vdf_node.call(_.fp, name.encode('utf-8') )
467 def edon(_):
468 libcxr_vdf_edon.call(_.fp)
469 def kv(_,k,v):
470 libcxr_vdf_kv.call(_.fp, k.encode('utf-8'), v.encode('utf-8'))
471
472 # Other
473 libcxr_lightpatch_bsp = extern( "cxr_lightpatch_bsp", [c_char_p], None )
474
475 libcxr_funcs = [ libcxr_decompose, libcxr_free_world, libcxr_begin_vmf, \
476 libcxr_vmf_begin_entities, libcxr_push_world_vmf, \
477 libcxr_end_vmf, libcxr_vdf_open, libcxr_vdf_close, \
478 libcxr_vdf_put, libcxr_vdf_node, libcxr_vdf_edon,
479 libcxr_vdf_kv, libcxr_lightpatch_bsp, libcxr_write_test_data,\
480 libcxr_world_preview, libcxr_free_tri_mesh ]
481
482 # Callbacks
483
484 def libcxr_log_callback(logStr):
485 print( F"{logStr.decode('utf-8')}",end='' )
486
487 cxr_line_positions = None
488 cxr_line_colours = None
489
490 def cxr_reset_lines():
491 global cxr_line_positions, cxr_line_colours
492
493 cxr_line_positions = []
494 cxr_line_colours = []
495
496 def cxr_batch_lines():
497 global cxr_line_positions, cxr_line_colours, cxr_view_shader, cxr_view_lines
498
499 cxr_view_lines = batch_for_shader(\
500 cxr_view_shader, 'LINES',\
501 { "pos": cxr_line_positions, "color": cxr_line_colours })
502
503 def libcxr_line_callback( p0,p1,colour ):
504 global cxr_line_colours, cxr_line_positions
505
506 cxr_line_positions += [(p0[0],p0[1],p0[2])]
507 cxr_line_positions += [(p1[0],p1[1],p1[2])]
508 cxr_line_colours += [(colour[0],colour[1],colour[2],colour[3])]
509 cxr_line_colours += [(colour[0],colour[1],colour[2],colour[3])]
510
511 def cxr_reset_all():
512 global cxr_jobs_inf, cxr_jobs_batch, cxr_error_inf, cxr_view_mesh
513 cxr_jobs_inf = None
514 cxr_jobs_batch = None
515 cxr_error_inf = None
516
517 cxr_reset_lines()
518 cxr_batch_lines()
519 cxr_view_mesh = None
520
521 scene_redraw()
522
523 # libnbvtf
524 # ------------------------------------------------------------------------------
525
526 libnbvtf = None
527
528 # Constants
529 NBVTF_IMAGE_FORMAT_ABGR8888 = 1
530 NBVTF_IMAGE_FORMAT_BGR888 = 3
531 NBVTF_IMAGE_FORMAT_DXT1 = 13
532 NBVTF_IMAGE_FORMAT_DXT5 = 15
533 NBVTF_TEXTUREFLAGS_CLAMPS = 0x00000004
534 NBVTF_TEXTUREFLAGS_CLAMPT = 0x00000008
535 NBVTF_TEXTUREFLAGS_NORMAL = 0x00000080
536 NBVTF_TEXTUREFLAGS_NOMIP = 0x00000100
537 NBVTF_TEXTUREFLAGS_NOLOD = 0x00000200
538
539 libnbvtf_convert = extern( "nbvtf_convert", \
540 [c_char_p,c_int32,c_int32,c_int32,c_int32,c_int32,c_uint32,c_char_p], \
541 c_int32 )
542
543 libnbvtf_init = extern( "nbvtf_init", [], None )
544 libnbvtf_funcs = [ libnbvtf_convert, libnbvtf_init ]
545
546 # Loading
547 # --------------------------
548
549 def shared_reload():
550 global libcxr, libnbvtf, libcxr_funcs, libnbvtf_funcs
551
552 # Unload libraries if existing
553 def _reload( lib, path ):
554 if CXR_GNU_LINUX==1:
555 if lib != None:
556 _handle = lib._handle
557 for i in range(10): libc_dlclose( _handle )
558 lib = None
559 del lib
560
561 libpath = F'{os.path.dirname(__file__)}/{path}{CXR_SHARED_EXT}'
562 return cdll.LoadLibrary( libpath )
563
564 libnbvtf = _reload( libnbvtf, "libnbvtf" )
565 libcxr = _reload( libcxr, "libcxr" )
566
567 for fd in libnbvtf_funcs:
568 fd.loadfrom( libnbvtf )
569 libnbvtf_init.call()
570
571 for fd in libcxr_funcs:
572 fd.loadfrom( libcxr )
573
574 # Callbacks
575 global c_libcxr_log_callback, c_libcxr_line_callback
576
577 LOG_FUNCTION_TYPE = CFUNCTYPE(None,c_char_p)
578 c_libcxr_log_callback = LOG_FUNCTION_TYPE(libcxr_log_callback)
579
580 LINE_FUNCTION_TYPE = CFUNCTYPE(None,\
581 POINTER(c_double), POINTER(c_double), POINTER(c_double))
582 c_libcxr_line_callback = LINE_FUNCTION_TYPE(libcxr_line_callback)
583
584 libcxr.cxr_set_log_function(cast(c_libcxr_log_callback,c_void_p))
585 libcxr.cxr_set_line_function(cast(c_libcxr_line_callback,c_void_p))
586
587 build_time = c_char_p.in_dll(libcxr,'cxr_build_time')
588 print( F"libcxr build time: {build_time.value}" )
589
590 shared_reload()
591
592 # Configuration
593 # ------------------------------------------------------------------------------
594
595 # Standard entity functions, think of like base.fgd
596 #
597 def cxr_get_origin(context):
598 return context['object'].location * context['transform']['scale'] + \
599 mathutils.Vector(context['transform']['offset'])
600
601 def cxr_get_angles(context):
602 obj = context['object']
603 euler = [ a*57.295779513 for a in obj.rotation_euler ]
604 angle = [0,0,0]
605 angle[0] = euler[1]
606 angle[1] = euler[2]
607 angle[2] = euler[0]
608 return angle
609
610 def cxr_baseclass(classes, other):
611 base = other.copy()
612 for x in classes:
613 base.update(x.copy())
614 return base
615
616 def ent_soundscape(context):
617 obj = context['object']
618 kvs = cxr_baseclass([ent_origin],\
619 {
620 "radius": obj.scale.x * bpy.context.scene.cxr_data.scale_factor,
621 "soundscape": {"type":"string","default":""}
622 })
623
624 return kvs
625
626 # EEVEE Light component converter -> Source 1
627 #
628 def ent_lights(context):
629 obj = context['object']
630 kvs = cxr_baseclass([ent_origin],\
631 {
632 "_distance": (0.0 if obj.data.cxr_data.realtime else -1.0),
633 "_lightHDR": '-1 -1 -1 1',
634 "_lightscaleHDR": 1
635 })
636
637 light_base = [(pow(obj.data.color[i],1.0/2.2)*255.0) for i in range(3)] +\
638 [obj.data.energy * bpy.context.scene.cxr_data.light_scale]
639
640 if obj.data.type == 'SPOT' or obj.data.type == 'SUN':
641 # Blenders directional lights are -z forward
642 # Source is +x, however, it seems to use a completely different system.
643 # Since we dont care about roll for spotlights, we just take the
644 # pitch and yaw via trig
645
646 _,mtx_rot,_ = obj.matrix_world.decompose()
647 fwd = mtx_rot @ mathutils.Vector((0,0,-1))
648 dir_pitch = math.asin(fwd[2]) * 57.295779513
649 dir_yaw = math.atan2(fwd[1],fwd[0]) * 57.295779513
650
651 if obj.data.type == 'SPOT':
652 kvs['_light'] = [ int(x) for x in light_base ]
653 kvs['_cone'] = obj.data.spot_size*(57.295779513/2.0)
654 kvs['_inner_cone'] = (1.0-obj.data.spot_blend)*kvs['_cone']
655
656 kvs['pitch'] = dir_pitch
657 kvs['angles'] = [ 0, dir_yaw, 0 ]
658 kvs['_quadratic_attn'] = 0.0 # Source spotlights + quadratic falloff look
659 # Really bad...
660 #
661 # Blender's default has a much more 'nice'
662 # look.
663 kvs['_linear_attn'] = 1.0
664
665 elif obj.data.type == 'POINT':
666 kvs['_light'] = [ int(x) for x in light_base]
667 kvs['_quadratic_attn'] = 1.0
668 kvs['_linear_attn'] = 1.0
669
670 elif obj.data.type == 'SUN':
671 light_base[3] *= 300.0 * 5
672 kvs['_light'] = [ int(x) for x in light_base ]
673
674 ambient = bpy.context.scene.world.color
675 kvs['_ambient'] = [int(pow(ambient[i],1.0/2.2)*255.0) for i in range(3)] +\
676 [80 * 5]
677 kvs['_ambientHDR'] = [-1,-1,-1,1]
678 kvs['_AmbientScaleHDR'] = 1
679 kvs['pitch'] = dir_pitch
680 kvs['angles'] = [ dir_pitch, dir_yaw, 0.0 ]
681 kvs['SunSpreadAngle'] = 0
682
683 return kvs
684
685 def ent_prop(context):
686 if isinstance( context['object'], bpy.types.Collection ):
687 kvs = {}
688 target = context['object']
689 pos = mathutils.Vector(context['origin'])
690 pos += mathutils.Vector(context['transform']['offset'])
691
692 kvs['origin'] = [pos[1],-pos[0],pos[2]]
693 kvs['angles'] = [0,180,0]
694 kvs['uniformscale'] = 1.0
695 else:
696 kvs = cxr_baseclass([ent_origin],{})
697 target = context['object'].instance_collection
698
699 obj = context['object']
700 euler = [ a*57.295779513 for a in obj.rotation_euler ]
701 angle = [0,0,0]
702 angle[0] = euler[1]
703 angle[1] = euler[2] + 180.0 # Dunno...
704 angle[2] = euler[0]
705
706 kvs['angles'] = angle
707 kvs['uniformscale'] = obj.scale[0]
708
709 if target.cxr_data.shadow_caster:
710 kvs['enablelightbounce'] = 1
711 kvs['disableshadows'] = 0
712 else:
713 kvs['enablelightbounce'] = 0
714 kvs['disableshadows'] = 1
715
716 kvs['fademindist'] = -1
717 kvs['fadescale'] = 1
718 kvs['model'] = F"{asset_path('models',target)}.mdl".lower()
719 kvs['renderamt'] = 255
720 kvs['rendercolor'] = [255, 255, 255]
721 kvs['skin'] = 0
722 kvs['solid'] = 6
723
724 return kvs
725
726 def ent_sky_camera(context):
727 settings = bpy.context.scene.cxr_data
728 scale = settings.scale_factor / settings.skybox_scale_factor
729
730 kvs = {
731 "origin": [_ for _ in context['transform']['offset']],
732 "angles": [ 0, 0, 0 ],
733 "fogcolor": [255, 255, 255],
734 "fogcolor2": [255, 255, 255],
735 "fogdir": [1,0,0],
736 "fogend": 2000.0,
737 "fogmaxdensity": 1,
738 "fogstart": 500.0,
739 "HDRColorScale": 1.0,
740 "scale": scale
741 }
742 return kvs
743
744 def ent_cubemap(context):
745 obj = context['object']
746 return cxr_baseclass([ent_origin], {"cubemapsize": obj.data.cxr_data.size})
747
748 ent_origin = { "origin": cxr_get_origin }
749 ent_angles = { "angles": cxr_get_angles }
750 ent_transform = cxr_baseclass( [ent_origin], ent_angles )
751
752 #include the user config
753 exec(open(F'{os.path.dirname(__file__)}/config.py').read())
754
755 # Blender state callbacks
756 # ------------------------------------------------------------------------------
757
758 @persistent
759 def cxr_on_load(dummy):
760 global cxr_view_lines, cxr_view_mesh
761
762 cxr_view_lines = None
763 cxr_view_mesh = None
764
765 @persistent
766 def cxr_dgraph_update(scene,dgraph):
767 return
768 print( F"Hallo {time.time()}" )
769
770 # Convexer compilation functions
771 # ------------------------------------------------------------------------------
772
773 # Asset path management
774
775 def asset_uid(asset):
776 if isinstance(asset,str):
777 return asset
778
779 # Create a unique ID string
780 base = "bopshei"
781 v = asset.cxr_data.asset_id
782 name = ""
783
784 if v == 0:
785 name = "a"
786 else:
787 dig = []
788
789 while v:
790 dig.append( int( v % len(base) ) )
791 v //= len(base)
792
793 for d in dig[::-1]:
794 name += base[d]
795
796 if bpy.context.scene.cxr_data.include_names:
797 name += asset.name.replace('.','_')
798
799 return name
800
801 # -> <project_name>/<asset_name>
802 def asset_name(asset):
803 return F"{bpy.context.scene.cxr_data.project_name}/{asset_uid(asset)}"
804
805 # -> <subdir>/<project_name>/<asset_name>
806 def asset_path(subdir, asset):
807 return F"{subdir}/{asset_name(asset_uid(asset))}"
808
809 # -> <csgo>/<subdir>/<project_name>/<asset_name>
810 def asset_full_path(sdir,asset):
811 return F"{bpy.context.scene.cxr_data.subdir}/"+\
812 F"{asset_path(sdir,asset_uid(asset))}"
813
814 # Decomposes mesh, and sets global error information if failed.
815 # - returns None on fail
816 # - returns world on success
817 def cxr_decompose_globalerr( mesh_src ):
818 global cxr_error_inf
819
820 err = c_int32(0)
821 world = libcxr_decompose.call( mesh_src, pointer(err) )
822
823 if world == None:
824 cxr_view_mesh = None
825 cxr_batch_lines()
826
827 cxr_error_inf = [\
828 ("No Error", "There is no error?"),\
829 ("Bad input", "Non manifold geometry is present in the input mesh"),\
830 ("Bad result","An invalid manifold was generated, try to simplify"),\
831 ("Bad result","Make sure there is a clear starting point"),\
832 ("Bad result","Implicit vertex was invalid, try to simplify"),\
833 ("Bad input","Non coplanar vertices are in the source mesh"),\
834 ("Bad input","Non convex polygon is in the source mesh"),\
835 ("Bad result","Undefined failure"),\
836 ("Invalid Input", "Undefined failure"),\
837 ][err.value]
838
839 scene_redraw()
840
841 return world
842
843 # Entity functions / infos
844 # ------------------------
845
846 def cxr_collection_purpose(collection):
847 if collection.name.startswith('.'): return None
848 if collection.hide_render: return None
849 if collection.name.startswith('mdl_'): return 'model'
850 return 'group'
851
852 def cxr_object_purpose(obj):
853 objpurpose = None
854 group = None
855
856 def _search(collection):
857 nonlocal objpurpose, group, obj
858
859 purpose = cxr_collection_purpose( collection )
860 if purpose == None: return
861 if purpose == 'model':
862 for o in collection.objects:
863 if o == obj:
864 if o.type != 'EMPTY':
865 objpurpose = 'model'
866 group = collection
867 return
868 return
869 for o in collection.objects:
870 if o == obj:
871 classname = cxr_classname(o)
872 if classname != None:
873 objpurpose = 'entity'
874 if o.type == 'MESH':
875 objpurpose = 'brush_entity'
876 group = collection
877 else:
878 if o.type == 'MESH':
879 objpurpose = 'brush'
880 group = collection
881 return
882 for c in collection.children:
883 _search(c)
884
885 if 'main' in bpy.data.collections:
886 _search( bpy.data.collections['main'] )
887
888 if objpurpose == None and 'skybox' in bpy.data.collections:
889 _search( bpy.data.collections['skybox'] )
890
891 return (group,objpurpose)
892
893 def cxr_intrinsic_classname(obj):
894 if obj.type == 'LIGHT':
895 return {
896 'SPOT': "light_spot",
897 'POINT': "light",
898 'SUN': "light_environment" }[ obj.data.type ]
899
900 elif obj.type == 'LIGHT_PROBE':
901 return "env_cubemap"
902 elif obj.type == 'EMPTY':
903 if obj.is_instancer:
904 return "prop_static"
905
906 return None
907
908 def cxr_custom_class(obj):
909 if obj.type == 'MESH': custom_class = obj.cxr_data.brushclass
910 else: custom_class = obj.cxr_data.classname
911
912 return custom_class
913
914 def cxr_classname(obj):
915 intr = cxr_intrinsic_classname(obj)
916 if intr != None: return intr
917
918 custom_class = cxr_custom_class(obj)
919 if custom_class != 'NONE':
920 return custom_class
921
922 return None
923
924 # Returns array of:
925 # intinsic: (k, False, value)
926 # property: (k, True, value or default)
927 #
928 # Error: None
929 #
930 def cxr_entity_keyvalues(context):
931 classname = context['classname']
932 obj = context['object']
933 if classname not in cxr_entities: return None
934
935 result = []
936
937 entdef = cxr_entities[classname]
938 kvs = entdef['keyvalues']
939
940 if callable(kvs): kvs = kvs(context)
941
942 for k in kvs:
943 kv = kvs[k]
944 value = kv
945 isprop = False
946
947 if isinstance(kv,dict):
948 isprop = True
949 value = obj[ F"cxrkv_{k}" ]
950 else:
951 if callable(kv):
952 value = kv(context)
953
954 if isinstance(value,mathutils.Vector):
955 value = [_ for _ in value]
956
957 result += [(k, isprop, value)]
958
959 return result
960
961 # Extract material information from shader graph data
962 #
963 def material_info(mat):
964 info = {}
965 info['res'] = (512,512)
966 info['name'] = 'tools/toolsnodraw'
967
968 if mat == None or mat.use_nodes == False:
969 return info
970
971 # Builtin shader
972 if mat.cxr_data.shader == 'Builtin':
973 info['name'] = mat.name
974 return info
975
976 # Custom materials
977 info['name'] = asset_name(mat)
978
979 # Using the cxr_graph_mapping as a reference, go through the shader
980 # graph and gather all $props from it.
981 #
982 def _graph_read( node_def, node=None, depth=0 ):
983 nonlocal mat
984 nonlocal info
985
986 def _variant_apply( val ):
987 nonlocal mat
988
989 if isinstance( val, list ):
990 for shader_variant in val:
991 if shader_variant[0] == mat.cxr_data.shader:
992 return shader_variant[1]
993 return val[0][1]
994 else:
995 return val
996
997 # Find rootnodes
998 if node == None:
999 _graph_read.extracted = []
1000
1001 for node_idname in node_def:
1002 for n in mat.node_tree.nodes:
1003 if n.name == node_idname:
1004 node_def = node_def[node_idname]
1005 node = n
1006 break
1007
1008 for link in node_def:
1009 link_def = _variant_apply( node_def[link] )
1010
1011 if isinstance( link_def, dict ):
1012 node_link = node.inputs[link]
1013
1014 if node_link.is_linked:
1015
1016 # look for definitions for the connected node type
1017 from_node = node_link.links[0].from_node
1018
1019 node_name = from_node.name.split('.')[0]
1020 if node_name in link_def:
1021 from_node_def = link_def[ node_name ]
1022
1023 _graph_read( from_node_def, from_node, depth+1 )
1024
1025 # No definition! :(
1026 # TODO: Make a warning for this?
1027
1028 else:
1029 if "default" in link_def:
1030 prop = _variant_apply( link_def['default'] )
1031 info[prop] = node_link.default_value
1032 else:
1033 prop = _variant_apply( link_def )
1034 info[prop] = getattr( node, link )
1035
1036 _graph_read(cxr_graph_mapping)
1037
1038 if "$basetexture" in info:
1039 export_res = info['$basetexture'].cxr_data.export_res
1040 info['res'] = (export_res[0], export_res[1])
1041
1042 return info
1043
1044 def vec3_min( a, b ):
1045 return mathutils.Vector((min(a[0],b[0]),min(a[1],b[1]),min(a[2],b[2])))
1046 def vec3_max( a, b ):
1047 return mathutils.Vector((max(a[0],b[0]),max(a[1],b[1]),max(a[2],b[2])))
1048
1049 def cxr_collection_center(collection, transform):
1050 BIG=999999999
1051 bounds_min = mathutils.Vector((BIG,BIG,BIG))
1052 bounds_max = mathutils.Vector((-BIG,-BIG,-BIG))
1053
1054 for obj in collection.objects:
1055 if obj.type == 'MESH':
1056 corners = [ mathutils.Vector(c) for c in obj.bound_box ]
1057
1058 for corner in [ obj.matrix_world@c for c in corners ]:
1059 bounds_min = vec3_min( bounds_min, corner )
1060 bounds_max = vec3_max( bounds_max, corner )
1061
1062 center = (bounds_min + bounds_max) / 2.0
1063
1064 origin = mathutils.Vector((-center[1],center[0],center[2]))
1065 origin *= transform['scale']
1066
1067 return origin
1068
1069 # Prepares Scene into dictionary format
1070 #
1071 def cxr_scene_collect():
1072 context = bpy.context
1073
1074 # Make sure all of our asset types have a unique ID
1075 def _uid_prepare(objtype):
1076 used_ids = [0]
1077 to_generate = []
1078 id_max = 0
1079 for o in objtype:
1080 vs = o.cxr_data
1081 if vs.asset_id in used_ids:
1082 to_generate+=[vs]
1083 else:
1084 id_max = max(id_max,vs.asset_id)
1085 used_ids+=[vs.asset_id]
1086 for vs in to_generate:
1087 id_max += 1
1088 vs.asset_id = id_max
1089 _uid_prepare(bpy.data.materials)
1090 _uid_prepare(bpy.data.images)
1091 _uid_prepare(bpy.data.collections)
1092
1093 sceneinfo = {
1094 "entities": [], # Everything with a classname
1095 "geo": [], # All meshes without a classname
1096 "heros": [] # Collections prefixed with mdl_
1097 }
1098
1099 def _collect(collection,transform):
1100 nonlocal sceneinfo
1101
1102 purpose = cxr_collection_purpose( collection )
1103 if purpose == None: return
1104 if purpose == 'model':
1105 sceneinfo['entities'] += [{
1106 "object": collection,
1107 "classname": "prop_static",
1108 "transform": transform,
1109 "origin": cxr_collection_center( collection, transform )
1110 }]
1111
1112 sceneinfo['heros'] += [{
1113 "collection": collection,
1114 "transform": transform,
1115 "origin": cxr_collection_center( collection, transform )
1116 }]
1117 return
1118
1119 for obj in collection.objects:
1120 if obj.hide_get(): continue
1121
1122 classname = cxr_classname( obj )
1123
1124 if classname != None:
1125 sceneinfo['entities'] += [{
1126 "object": obj,
1127 "classname": classname,
1128 "transform": transform
1129 }]
1130 elif obj.type == 'MESH':
1131 sceneinfo['geo'] += [{
1132 "object": obj,
1133 "transform": transform
1134 }]
1135
1136 for c in collection.children:
1137 _collect( c, transform )
1138
1139 transform_main = {
1140 "scale": context.scene.cxr_data.scale_factor,
1141 "offset": (0,0,0)
1142 }
1143
1144 transform_sky = {
1145 "scale": context.scene.cxr_data.skybox_scale_factor,
1146 "offset": (0,0,context.scene.cxr_data.skybox_offset )
1147 }
1148
1149 if 'main' in bpy.data.collections:
1150 _collect( bpy.data.collections['main'], transform_main )
1151
1152 if 'skybox' in bpy.data.collections:
1153 _collect( bpy.data.collections['skybox'], transform_sky )
1154
1155 sceneinfo['entities'] += [{
1156 "object": None,
1157 "transform": transform_sky,
1158 "classname": "sky_camera"
1159 }]
1160
1161 return sceneinfo
1162
1163 # Write VMF out to file (JOB HANDLER)
1164 #
1165 def cxr_export_vmf(sceneinfo, output_vmf):
1166 cxr_reset_lines()
1167
1168 with vdf_structure(output_vmf) as m:
1169 print( F"Write: {output_vmf}" )
1170
1171 vmfinfo = cxr_vmf_context()
1172 vmfinfo.mapversion = 4
1173
1174 #TODO: These need to be in options...
1175 vmfinfo.skyname = bpy.context.scene.cxr_data.skyname.encode('utf-8')
1176 vmfinfo.detailvbsp = b"detail.vbsp"
1177 vmfinfo.detailmaterial = b"detail/detailsprites"
1178 vmfinfo.lightmap_scale = 12
1179
1180 vmfinfo.brush_count = 0
1181 vmfinfo.entity_count = 0
1182 vmfinfo.face_count = 0
1183
1184 visgroups = (cxr_visgroup*len(cxr_visgroups))()
1185 for i, vg in enumerate(cxr_visgroups):
1186 visgroups[i].name = vg.encode('utf-8')
1187 vmfinfo.visgroups = cast(visgroups, POINTER(cxr_visgroup))
1188 vmfinfo.visgroup_count = len(cxr_visgroups)
1189
1190 libcxr_begin_vmf.call( pointer(vmfinfo), m.fp )
1191
1192 def _buildsolid( cmd ):
1193 nonlocal m
1194
1195 print( F"{vmfinfo.brush_count} :: {cmd['object'].name}" )
1196
1197 baked = mesh_cxr_format( cmd['object'] )
1198 world = cxr_decompose_globalerr( baked )
1199
1200 if world == None:
1201 return False
1202
1203 vmfinfo.scale = cmd['transform']['scale']
1204
1205 offset = cmd['transform']['offset']
1206 vmfinfo.offset[0] = offset[0]
1207 vmfinfo.offset[1] = offset[1]
1208 vmfinfo.offset[2] = offset[2]
1209
1210 if cmd['object'].cxr_data.lightmap_override > 0:
1211 vmfinfo.lightmap_scale = cmd['object'].cxr_data.lightmap_override
1212 else:
1213 vmfinfo.lightmap_scale = bpy.context.scene.cxr_data.lightmap_scale
1214
1215 libcxr_push_world_vmf.call( world, pointer(vmfinfo), m.fp )
1216 libcxr_free_world.call( world )
1217
1218 return True
1219
1220 # World geometry
1221 for brush in sceneinfo['geo']:
1222 vmfinfo.visgroupid = int(brush['object'].cxr_data.visgroup)
1223 if not _buildsolid( brush ):
1224 cxr_batch_lines()
1225 scene_redraw()
1226 return False
1227 vmfinfo.visgroupid = 0
1228
1229 libcxr_vmf_begin_entities.call(pointer(vmfinfo), m.fp)
1230
1231 # Entities
1232 for ent in sceneinfo['entities']:
1233 obj = ent['object']
1234 ctx = ent['transform']
1235 cls = ent['classname']
1236
1237 m.node( 'entity' )
1238 m.kv( 'classname', cls )
1239
1240 kvs = cxr_entity_keyvalues( ent )
1241
1242 for kv in kvs:
1243 if isinstance(kv[2], list):
1244 m.kv( kv[0], ' '.join([str(_) for _ in kv[2]]) )
1245 else: m.kv( kv[0], str(kv[2]) )
1246
1247 if obj == None:
1248 pass
1249 elif not isinstance( obj, bpy.types.Collection ):
1250 if obj.type == 'MESH':
1251 vmfinfo.visgroupid = int(obj.cxr_data.visgroup)
1252 if not _buildsolid( ent ):
1253 cxr_batch_lines()
1254 scene_redraw()
1255 return False
1256
1257 if obj != None:
1258 m.node( 'editor' )
1259 m.kv( 'visgroupid', str(obj.cxr_data.visgroup) )
1260 m.kv( 'visgroupshown', '1' )
1261 m.kv( 'visgroupautoshown', '1' )
1262 m.edon()
1263
1264 m.edon()
1265 vmfinfo.visgroupid = 0
1266
1267 print( "Done" )
1268 return True
1269
1270 # COmpile image using NBVTF and hash it (JOB HANDLER)
1271 #
1272 def compile_image(img):
1273 if img==None:
1274 return None
1275
1276 name = asset_name(img)
1277 src_path = bpy.path.abspath(img.filepath)
1278
1279 dims = img.cxr_data.export_res
1280 fmt = {
1281 'RGBA': NBVTF_IMAGE_FORMAT_ABGR8888,
1282 'DXT1': NBVTF_IMAGE_FORMAT_DXT1,
1283 'DXT5': NBVTF_IMAGE_FORMAT_DXT5,
1284 'RGB': NBVTF_IMAGE_FORMAT_BGR888
1285 }[ img.cxr_data.fmt ]
1286
1287 mipmap = img.cxr_data.mipmap
1288 lod = img.cxr_data.lod
1289 clamp = img.cxr_data.clamp
1290 flags = img.cxr_data.flags
1291
1292 q=bpy.context.scene.cxr_data.image_quality
1293
1294 userflag_hash = F"{mipmap}.{lod}.{clamp}.{flags}"
1295 file_hash = F"{name}.{os.path.getmtime(src_path)}"
1296 comphash = F"{file_hash}.{dims[0]}.{dims[1]}.{fmt}.{userflag_hash}.{q}"
1297
1298 if img.cxr_data.last_hash != comphash:
1299 print( F"Texture update: {img.filepath}" )
1300
1301 src = src_path.encode('utf-8')
1302 dst = (asset_full_path('materials',img)+'.vtf').encode('utf-8')
1303
1304 flags_full = flags
1305
1306 # texture setting flags
1307 if not lod: flags_full |= NBVTF_TEXTUREFLAGS_NOLOD
1308 if clamp:
1309 flags_full |= NBVTF_TEXTUREFLAGS_CLAMPS
1310 flags_full |= NBVTF_TEXTUREFLAGS_CLAMPT
1311
1312 if libnbvtf_convert.call(src,dims[0],dims[1],mipmap,fmt,q,flags_full,dst):
1313 img.cxr_data.last_hash = comphash
1314
1315 return name
1316
1317 #
1318 # Compile a material to VMT format. This is quick to run, doesnt need to be a
1319 # job handler.
1320 #
1321 def compile_material(mat):
1322 info = material_info(mat)
1323 properties = mat.cxr_data
1324
1325 print( F"Compile {asset_full_path('materials',mat)}.vmt" )
1326 if properties.shader == 'Builtin':
1327 return []
1328
1329 props = []
1330
1331 # Walk the property tree
1332 def _mlayer( layer ):
1333 nonlocal properties, props
1334
1335 for decl in layer:
1336 if isinstance(layer[decl],dict): # $property definition
1337 pdef = layer[decl]
1338 ptype = pdef['type']
1339
1340 subdefines = False
1341 default = None
1342 prop = None
1343
1344 if 'shaders' in pdef and properties.shader not in pdef['shaders']:
1345 continue
1346
1347 # Group expansion (does it have subdefinitions?)
1348 for ch in pdef:
1349 if isinstance(pdef[ch],dict):
1350 subdefines = True
1351 break
1352
1353 expandview = False
1354
1355 if ptype == 'ui':
1356 expandview = True
1357 else:
1358 if ptype == 'intrinsic':
1359 if decl in info:
1360 prop = info[decl]
1361 else:
1362 prop = getattr(properties,decl)
1363 default = pdef['default']
1364
1365 if not isinstance(prop,str) and \
1366 not isinstance(prop,bpy.types.Image) and \
1367 hasattr(prop,'__getitem__'):
1368 prop = tuple([p for p in prop])
1369
1370 if prop != default:
1371 # write prop
1372 props += [(decl,pdef,prop)]
1373
1374 if subdefines:
1375 expandview = True
1376
1377 if expandview: _mlayer(pdef)
1378
1379 _mlayer( cxr_shader_params )
1380
1381 # Write the vmt
1382 with vdf_structure( F"{asset_full_path('materials',mat)}.vmt" ) as vmt:
1383 vmt.node( properties.shader )
1384 vmt.put( "// Convexer export\n" )
1385
1386 for pair in props:
1387 decl = pair[0]
1388 pdef = pair[1]
1389 prop = pair[2]
1390
1391 def _numeric(v):
1392 nonlocal pdef
1393 if 'exponent' in pdef: return str(pow( v, pdef['exponent'] ))
1394 else: return str(v)
1395
1396 if isinstance(prop,bpy.types.Image):
1397 vmt.kv( decl, asset_name(prop))
1398 elif isinstance(prop,bool):
1399 vmt.kv( decl, '1' if prop else '0' )
1400 elif isinstance(prop,str):
1401 vmt.kv( decl, prop )
1402 elif isinstance(prop,float) or isinstance(prop,int):
1403 vmt.kv( decl, _numeric(prop) )
1404 elif isinstance(prop,tuple):
1405 vmt.kv( decl, F"[{' '.join([_numeric(_) for _ in prop])}]" )
1406 else:
1407 vmt.put( F"// (cxr) unkown shader value type'{type(prop)}'" )
1408
1409 vmt.edon()
1410 return props
1411
1412 def cxr_modelsrc_vphys( mdl ):
1413 for obj in mdl.objects:
1414 if obj.name == F"{mdl.name}_phy":
1415 return obj
1416 return None
1417
1418 def cxr_export_modelsrc( mdl, origin, asset_dir, project_name, transform ):
1419 dgraph = bpy.context.evaluated_depsgraph_get()
1420
1421 # Compute hash value
1422 chash = asset_uid(mdl)+str(origin)+str(transform)
1423
1424 #for obj in mdl.objects:
1425 # if obj.type != 'MESH':
1426 # continue
1427
1428 # ev = obj.evaluated_get(dgraph).data
1429 # srcverts=[(v.co[0],v.co[1],v.co[2]) for v in ev.vertices]
1430 # srcloops=[(l.normal[0],l.normal[1],l.normal[2]) for l in ev.loops]
1431
1432 # chash=hashlib.sha224((str(srcverts)+chash).encode()).hexdigest()
1433 # chash=hashlib.sha224((str(srcloops)+chash).encode()).hexdigest()
1434
1435 # if ev.uv_layers.active != None:
1436 # uv_layer = ev.uv_layers.active.data
1437 # srcuv=[(uv.uv[0],uv.uv[1]) for uv in uv_layer]
1438 # else:
1439 # srcuv=['none']
1440
1441 # chash=hashlib.sha224((str(srcuv)+chash).encode()).hexdigest()
1442 # srcmats=[ ms.material.name for ms in obj.material_slots ]
1443 # chash=hashlib.sha224((str(srcmats)+chash).encode()).hexdigest()
1444 # transforms=[ obj.location, obj.rotation_euler, obj.scale ]
1445 # srctr=[(v[0],v[1],v[2]) for v in transforms]
1446 # chash=hashlib.sha224((str(srctr)+chash).encode()).hexdigest()
1447
1448 #if chash != mdl.cxr_data.last_hash:
1449 # mdl.cxr_data.last_hash = chash
1450 # print( F"Compile: {mdl.name}" )
1451 #else:
1452 # return True
1453
1454 bpy.ops.object.select_all(action='DESELECT')
1455
1456 # Get viewlayer
1457 def _get_layer(col,name):
1458 for c in col.children:
1459 if c.name == name:
1460 return c
1461 sub = _get_layer(c,name)
1462 if sub != None:
1463 return sub
1464 return None
1465 layer = _get_layer(bpy.context.view_layer.layer_collection,mdl.name)
1466
1467 prev_state = layer.hide_viewport
1468 layer.hide_viewport=False
1469
1470 # Collect materials to be compiled, and temp rename for export
1471 mat_dict = {}
1472
1473 vphys = None
1474 for obj in mdl.objects:
1475 if obj.name == F"{mdl.name}_phy":
1476 vphys = obj
1477 continue
1478
1479 obj.select_set(state=True)
1480 for ms in obj.material_slots:
1481 if ms.material != None:
1482 if ms.material not in mat_dict:
1483 mat_dict[ms.material] = ms.material.name
1484 ms.material.name = asset_uid(ms.material)
1485 ms.material.use_nodes = False
1486
1487 uid=asset_uid(mdl)
1488 bpy.ops.export_scene.fbx( filepath=F'{asset_dir}/{uid}_ref.fbx',\
1489 check_existing=False,
1490 use_selection=True,
1491 apply_unit_scale=False,
1492 bake_space_transform=False
1493 )
1494
1495 bpy.ops.object.select_all(action='DESELECT')
1496
1497 if vphys != None:
1498 vphys.select_set(state=True)
1499 bpy.ops.export_scene.fbx( filepath=F'{asset_dir}/{uid}_phy.fbx',\
1500 check_existing=False,
1501 use_selection=True,
1502 apply_unit_scale=False,
1503 bake_space_transform=False
1504 )
1505 bpy.ops.object.select_all(action='DESELECT')
1506
1507 # Fix material names back to original
1508 for mat in mat_dict:
1509 mat.name = mat_dict[mat]
1510 mat.use_nodes = True
1511
1512 layer.hide_viewport=prev_state
1513
1514 # Write out QC file
1515 with open(F'{asset_dir}/{uid}.qc','w') as o:
1516 o.write(F'$modelname "{project_name}/{uid}"\n')
1517 #o.write(F'$scale .32\n')
1518 o.write(F'$scale {transform["scale"]/100.0}\n')
1519 o.write(F'$body _ "{uid}_ref.fbx"\n')
1520 o.write(F'$staticprop\n')
1521 o.write(F'$origin {origin[0]:.6f} {origin[1]:.6f} {origin[2]:.6f}\n')
1522
1523 if mdl.cxr_data.preserve_order:
1524 o.write(F"$preservetriangleorder\n")
1525
1526 if mdl.cxr_data.texture_shadows:
1527 o.write(F"$casttextureshadows\n")
1528
1529 o.write(F"$surfaceprop {mdl.cxr_data.surfaceprop}\n")
1530
1531 if vphys != None:
1532 o.write(F'$collisionmodel "{uid}_phy.fbx"\n')
1533 o.write("{\n")
1534 o.write(" $concave\n")
1535 o.write("}\n")
1536
1537 o.write(F'$cdmaterials {project_name}\n')
1538 o.write(F'$sequence idle {uid}_ref.fbx\n')
1539
1540 return True
1541 #
1542 # Copy bsp file (and also lightpatch it)
1543 #
1544 def cxr_patchmap( src, dst ):
1545 libcxr_lightpatch_bsp.call( src.encode('utf-8') )
1546 shutil.copyfile( src, dst )
1547 return True
1548
1549 # Convexer operators
1550 # ------------------------------------------------------------------------------
1551
1552 # Force reload of shared libraries
1553 #
1554 class CXR_RELOAD(bpy.types.Operator):
1555 bl_idname="convexer.reload"
1556 bl_label="Reload"
1557 def execute(_,context):
1558 shared_reload()
1559 return {'FINISHED'}
1560
1561 # Reset all debugging/ui information
1562 #
1563 class CXR_RESET(bpy.types.Operator):
1564 bl_idname="convexer.reset"
1565 bl_label="Reset Convexer"
1566 def execute(_,context):
1567 cxr_reset_all()
1568 return {'FINISHED'}
1569
1570 # Used for exporting data to use with ASAN builds
1571 #
1572 class CXR_DEV_OPERATOR(bpy.types.Operator):
1573 bl_idname="convexer.dev_test"
1574 bl_label="Export development data"
1575
1576 def execute(_,context):
1577 # Prepare input data
1578 mesh_src = mesh_cxr_format(context.active_object)
1579 libcxr_write_test_data.call( pointer(mesh_src) )
1580 return {'FINISHED'}
1581
1582 # UI: Preview how the brushes will looks in 3D view
1583 #
1584 class CXR_PREVIEW_OPERATOR(bpy.types.Operator):
1585 bl_idname="convexer.preview"
1586 bl_label="Preview Brushes"
1587
1588 RUNNING = False
1589
1590 def execute(_,context):
1591 global cxr_view_mesh
1592 global cxr_view_shader, cxr_view_mesh, cxr_error_inf
1593
1594 cxr_reset_all()
1595
1596 static = _.__class__
1597
1598 mesh_src = mesh_cxr_format(context.active_object)
1599 world = cxr_decompose_globalerr( mesh_src )
1600
1601 if world == None:
1602 return {'FINISHED'}
1603
1604 # Generate preview using cxr
1605 #
1606 ptrpreview = libcxr_world_preview.call( world )
1607 preview = ptrpreview[0]
1608
1609 vertices = preview.vertices[:preview.vertex_count]
1610 vertices = [(_[0],_[1],_[2]) for _ in vertices]
1611
1612 colours = preview.colours[:preview.vertex_count]
1613 colours = [(_[0],_[1],_[2],_[3]) for _ in colours]
1614
1615 indices = preview.indices[:preview.indices_count]
1616 indices = [ (indices[i*3+0],indices[i*3+1],indices[i*3+2]) \
1617 for i in range(int(preview.indices_count/3)) ]
1618
1619 cxr_view_mesh = batch_for_shader(
1620 cxr_view_shader, 'TRIS',
1621 { "pos": vertices, "color": colours },
1622 indices = indices,
1623 )
1624
1625 libcxr_free_tri_mesh.call( ptrpreview )
1626 libcxr_free_world.call( world )
1627 cxr_batch_lines()
1628 scene_redraw()
1629
1630 return {'FINISHED'}
1631
1632 # Search for VMF compiler executables in subdirectory
1633 #
1634 class CXR_DETECT_COMPILERS(bpy.types.Operator):
1635 bl_idname="convexer.detect_compilers"
1636 bl_label="Find compilers"
1637
1638 def execute(self,context):
1639 scene = context.scene
1640 settings = scene.cxr_data
1641 subdir = settings.subdir
1642
1643 for exename in ['studiomdl','vbsp','vvis','vrad']:
1644 searchpath = os.path.normpath(F'{subdir}/../bin/{exename}.exe')
1645 if os.path.exists(searchpath):
1646 settings[F'exe_{exename}'] = searchpath
1647
1648 return {'FINISHED'}
1649
1650 def cxr_compiler_path( compiler ):
1651 settings = bpy.context.scene.cxr_data
1652 subdir = settings.subdir
1653 path = os.path.normpath(F'{subdir}/../bin/{compiler}.exe')
1654
1655 if os.path.exists( path ): return path
1656 else: return None
1657
1658 # Compatibility layer
1659 #
1660 def cxr_temp_file( fn ):
1661 if CXR_GNU_LINUX == 1:
1662 return F"/tmp/fn"
1663 else:
1664 filepath = bpy.data.filepath
1665 directory = os.path.dirname(filepath)
1666 return F"{directory}/{fn}"
1667
1668 def cxr_winepath( path ):
1669 if CXR_GNU_LINUX == 1:
1670 return 'z:'+path.replace('/','\\')
1671 else:
1672 return path
1673
1674 # Main compile function
1675 #
1676 class CXR_COMPILER_CHAIN(bpy.types.Operator):
1677 bl_idname="convexer.chain"
1678 bl_label="Compile Chain"
1679
1680 # 'static'
1681 USER_EXIT = False
1682 SUBPROC = None
1683 TIMER = None
1684 TIMER_LAST = 0.0
1685 WAIT_REDRAW = False
1686 FILE = None
1687 LOG = []
1688
1689 JOBINFO = None
1690 JOBID = 0
1691 JOBSYS = None
1692
1693 def cancel(_,context):
1694 #global cxr_jobs_batch
1695 static = _.__class__
1696 wm = context.window_manager
1697
1698 if static.SUBPROC != None:
1699 static.SUBPROC.terminate()
1700 static.SUBPROC = None
1701
1702 if static.TIMER != None:
1703 wm.event_timer_remove( static.TIMER )
1704 static.TIMER = None
1705
1706 static.FILE.close()
1707
1708 #cxr_jobs_batch = None
1709 scene_redraw()
1710 return {'FINISHED'}
1711
1712 def modal(_,context,ev):
1713 static = _.__class__
1714
1715 if ev.type == 'TIMER':
1716 global cxr_jobs_batch, cxr_error_inf
1717
1718 if static.WAIT_REDRAW:
1719 scene_redraw()
1720 return {'PASS_THROUGH'}
1721 static.WAIT_REDRAW = True
1722
1723 if static.USER_EXIT:
1724 print( "Chain USER_EXIT" )
1725 return _.cancel(context)
1726
1727 if static.SUBPROC != None:
1728 # Deal with async modes
1729 status = static.SUBPROC.poll()
1730
1731 # Cannot redirect STDOUT through here without causing
1732 # undefined behaviour due to the Blender Python specification.
1733 #
1734 # Have to write it out to a file and read it back in.
1735 #
1736
1737 with open(cxr_temp_file("convexer_compile_log.txt"),"r") as log:
1738 static.LOG = log.readlines()
1739 if status == None:
1740 return {'PASS_THROUGH'}
1741 else:
1742 #for l in static.SUBPROC.stdout:
1743 # print( F'-> {l.decode("utf-8")}',end='' )
1744 static.SUBPROC = None
1745
1746 if status != 0:
1747 print(F'Compiler () error: {status}')
1748
1749 jobn = static.JOBSYS['jobs'][static.JOBID]
1750 cxr_error_inf = ( F"{static.JOBSYS['title']} error {status}", jobn )
1751
1752 return _.cancel(context)
1753
1754 static.JOBSYS['jobs'][static.JOBID] = None
1755 cxr_jobs_update_graph( static.JOBINFO )
1756 scene_redraw()
1757 return {'PASS_THROUGH'}
1758
1759 # Compile syncronous thing
1760 for sys in static.JOBINFO:
1761 for i,target in enumerate(sys['jobs']):
1762 if target != None:
1763
1764 if callable(sys['exec']):
1765 print( F"Run (sync): {static.JOBID} @{time.time()}" )
1766
1767 if not sys['exec'](*target):
1768 print( "Job failed" )
1769 return _.cancel(context)
1770
1771 sys['jobs'][i] = None
1772 static.JOBID += 1
1773 else:
1774 # Run external executable (wine)
1775 static.SUBPROC = subprocess.Popen( target,
1776 stdout=static.FILE,\
1777 stderr=subprocess.PIPE,\
1778 cwd=sys['cwd'])
1779 static.JOBSYS = sys
1780 static.JOBID = i
1781
1782 cxr_jobs_update_graph( static.JOBINFO )
1783 scene_redraw()
1784 return {'PASS_THROUGH'}
1785
1786 # All completed
1787 print( "All jobs completed!" )
1788 #cxr_jobs_batch = None
1789 #scene_redraw()
1790 return _.cancel(context)
1791
1792 return {'PASS_THROUGH'}
1793
1794 def invoke(_,context,event):
1795 global cxr_error_inf
1796
1797 static = _.__class__
1798 wm = context.window_manager
1799
1800 if static.TIMER != None:
1801 print("Chain exiting...")
1802 static.USER_EXIT=True
1803 return {'RUNNING_MODAL'}
1804
1805 print("Launching compiler toolchain")
1806 cxr_reset_all()
1807
1808 # Run static compilation units now (collect, vmt..)
1809 filepath = bpy.data.filepath
1810 directory = os.path.dirname(filepath)
1811 settings = bpy.context.scene.cxr_data
1812
1813 asset_dir = F"{directory}/modelsrc"
1814 material_dir = F"{settings.subdir}/materials/{settings.project_name}"
1815 model_dir = F"{settings.subdir}/models/{settings.project_name}"
1816 output_vmf = F"{directory}/{settings.project_name}.vmf"
1817
1818 bsp_local = F"{directory}/{settings.project_name}.bsp"
1819 bsp_remote = F"{settings.subdir}/maps/{settings.project_name}.bsp"
1820 bsp_packed = F"{settings.subdir}/maps/{settings.project_name}_pack.bsp"
1821 packlist = F"{directory}/{settings.project_name}_assets.txt"
1822
1823 os.makedirs( asset_dir, exist_ok=True )
1824 os.makedirs( material_dir, exist_ok=True )
1825 os.makedirs( model_dir, exist_ok=True )
1826
1827 static.FILE = open(cxr_temp_file("convexer_compile_log.txt"),"w")
1828 static.LOG = []
1829
1830 sceneinfo = cxr_scene_collect()
1831 image_jobs = []
1832 qc_jobs = []
1833
1834 # Collect materials
1835 a_materials = set()
1836 for brush in sceneinfo['geo']:
1837 for ms in brush['object'].material_slots:
1838 a_materials.add( ms.material )
1839 if ms.material.cxr_data.shader == 'VertexLitGeneric':
1840 errmat = ms.material.name
1841 errnam = brush['object'].name
1842
1843 cxr_error_inf = ( "Shader error", \
1844 F"Vertex shader ({errmat}) used on model ({errnam})" )
1845
1846 print( F"Vertex shader {errmat} used on {errnam}")
1847 scene_redraw()
1848 return {'CANCELLED'}
1849
1850 a_models = set()
1851 model_jobs = []
1852 for ent in sceneinfo['entities']:
1853 if ent['object'] == None: continue
1854
1855 if ent['classname'] == 'prop_static':
1856 obj = ent['object']
1857 if isinstance(obj,bpy.types.Collection):
1858 target = obj
1859 a_models.add( target )
1860 model_jobs += [(target, ent['origin'], asset_dir, \
1861 settings.project_name, ent['transform'])]
1862 else:
1863 target = obj.instance_collection
1864 if target in a_models:
1865 continue
1866 a_models.add( target )
1867
1868 # TODO: Should take into account collection instancing offset
1869 model_jobs += [(target, [0,0,0], asset_dir, \
1870 settings.project_name, ent['transform'])]
1871
1872 elif ent['object'].type == 'MESH':
1873 for ms in ent['object'].material_slots:
1874 a_materials.add( ms.material )
1875
1876 for mdl in a_models:
1877 uid = asset_uid(mdl)
1878 qc_jobs += [F'{uid}.qc']
1879
1880 for obj in mdl.objects:
1881 for ms in obj.material_slots:
1882 a_materials.add( ms.material )
1883 if ms.material.cxr_data.shader == 'LightMappedGeneric' or \
1884 ms.material.cxr_data.shader == 'WorldVertexTransition':
1885
1886 errmat = ms.material.name
1887 errnam = obj.name
1888
1889 cxr_error_inf = ( "Shader error", \
1890 F"Lightmapped shader ({errmat}) used on model ({errnam})" )
1891
1892 print( F"Lightmapped shader {errmat} used on {errnam}")
1893 scene_redraw()
1894 return {'CANCELLED'}
1895
1896 # Collect images
1897 for mat in a_materials:
1898 for pair in compile_material(mat):
1899 decl = pair[0]
1900 pdef = pair[1]
1901 prop = pair[2]
1902
1903 if isinstance(prop,bpy.types.Image):
1904 flags = 0
1905 if 'flags' in pdef: flags = pdef['flags']
1906 if prop not in image_jobs:
1907 image_jobs += [(prop,)]
1908 prop.cxr_data.flags = flags
1909
1910 # Create packlist
1911 with open( packlist, "w" ) as fp:
1912
1913 for mat in a_materials:
1914 if mat.cxr_data.shader == 'Builtin': continue
1915 fp.write(F"{asset_path('materials',mat)}.vmt\n")
1916 fp.write(F"{cxr_winepath(asset_full_path('materials',mat))}.vmt\n")
1917
1918 for img_job in image_jobs:
1919 img = img_job[0]
1920 fp.write(F"{asset_path('materials',img)}.vtf\n")
1921 fp.write(F"{cxr_winepath(asset_full_path('materials',img))}.vtf\n")
1922
1923 for mdl in a_models:
1924 local = asset_path('models',mdl)
1925 winep = cxr_winepath(asset_full_path('models',mdl))
1926
1927 fp.write(F"{local}.vvd\n")
1928 fp.write(F"{winep}.vvd\n")
1929 fp.write(F"{local}.dx90.vtx\n")
1930 fp.write(F"{winep}.dx90.vtx\n")
1931 fp.write(F"{local}.mdl\n")
1932 fp.write(F"{winep}.mdl\n")
1933 fp.write(F"{local}.vvd\n")
1934 fp.write(F"{winep}.vvd\n")
1935
1936 if cxr_modelsrc_vphys(mdl):
1937 fp.write(F"{local}.phy\n")
1938 fp.write(F"{winep}.phy\n")
1939
1940 # Convexer jobs
1941 static.JOBID = 0
1942 static.JOBINFO = []
1943
1944 if settings.comp_vmf:
1945 static.JOBINFO += [{
1946 "title": "Convexer",
1947 "w": 20,
1948 "colour": (0.863, 0.078, 0.235,1.0),
1949 "exec": cxr_export_vmf,
1950 "jobs": [(sceneinfo,output_vmf)]
1951 }]
1952
1953 if settings.comp_textures:
1954 if len(image_jobs) > 0:
1955 static.JOBINFO += [{
1956 "title": "Textures",
1957 "w": 40,
1958 "colour": (1.000, 0.271, 0.000,1.0),
1959 "exec": compile_image,
1960 "jobs": image_jobs
1961 }]
1962
1963 game = cxr_winepath( settings.subdir )
1964 args = [ \
1965 '-game', game, settings.project_name
1966 ]
1967
1968 # FBX stage
1969 if settings.comp_models:
1970 if len(model_jobs) > 0:
1971 static.JOBINFO += [{
1972 "title": "Batches",
1973 "w": 25,
1974 "colour": (1.000, 0.647, 0.000,1.0),
1975 "exec": cxr_export_modelsrc,
1976 "jobs": model_jobs
1977 }]
1978
1979 if len(qc_jobs) > 0:
1980 static.JOBINFO += [{
1981 "title": "StudioMDL",
1982 "w": 20,
1983 "colour": (1.000, 0.843, 0.000, 1.0),
1984 "exec": "studiomdl",
1985 "jobs": [[settings[F'exe_studiomdl']] + [\
1986 '-nop4', '-game', game, qc] for qc in qc_jobs],
1987 "cwd": asset_dir
1988 }]
1989
1990 # VBSP stage
1991 if settings.comp_compile:
1992 if not settings.opt_vbsp.startswith( 'disable' ):
1993 vbsp_opt = settings.opt_vbsp.split()
1994 static.JOBINFO += [{
1995 "title": "VBSP",
1996 "w": 25,
1997 "colour": (0.678, 1.000, 0.184,1.0),
1998 "exec": "vbsp",
1999 "jobs": [[settings[F'exe_vbsp']] + vbsp_opt + args],
2000 "cwd": directory
2001 }]
2002
2003 if not settings.opt_vvis.startswith( 'disable' ):
2004 vvis_opt = settings.opt_vvis.split()
2005 static.JOBINFO += [{
2006 "title": "VVIS",
2007 "w": 25,
2008 "colour": (0.000, 1.000, 0.498,1.0),
2009 "exec": "vvis",
2010 "jobs": [[settings[F'exe_vvis']] + vvis_opt + args ],
2011 "cwd": directory
2012 }]
2013
2014 if not settings.opt_vrad.startswith( 'disable' ):
2015 vrad_opt = settings.opt_vrad.split()
2016 static.JOBINFO += [{
2017 "title": "VRAD",
2018 "w": 25,
2019 "colour": (0.125, 0.698, 0.667,1.0),
2020 "exec": "vrad",
2021 "jobs": [[settings[F'exe_vrad']] + vrad_opt + args ],
2022 "cwd": directory
2023 }]
2024
2025 static.JOBINFO += [{
2026 "title": "CXR",
2027 "w": 5,
2028 "colour": (0.118, 0.565, 1.000,1.0),
2029 "exec": cxr_patchmap,
2030 "jobs": [(bsp_local,bsp_remote)]
2031 }]
2032
2033 if settings.comp_pack:
2034 static.JOBINFO += [{
2035 "title": "Pack",
2036 "w": 5,
2037 "colour": (0.541, 0.169, 0.886,1.0),
2038 "exec": "bspzip",
2039 "jobs": [[cxr_compiler_path("bspzip"), '-addlist', \
2040 cxr_winepath(bsp_remote),
2041 cxr_winepath(packlist),
2042 cxr_winepath(bsp_packed) ]],
2043 "cwd": directory
2044 }]
2045
2046 if len(static.JOBINFO) == 0:
2047 return {'CANCELLED'}
2048
2049 static.USER_EXIT=False
2050 static.TIMER=wm.event_timer_add(0.1,window=context.window)
2051 wm.modal_handler_add(_)
2052
2053 cxr_jobs_update_graph( static.JOBINFO )
2054 scene_redraw()
2055 return {'RUNNING_MODAL'}
2056
2057 class CXR_RESET_HASHES(bpy.types.Operator):
2058 bl_idname="convexer.hash_reset"
2059 bl_label="Reset asset hashes"
2060
2061 def execute(_,context):
2062 for c in bpy.data.collections:
2063 c.cxr_data.last_hash = F"<RESET>{time.time()}"
2064 c.cxr_data.asset_id=0
2065
2066 for t in bpy.data.images:
2067 t.cxr_data.last_hash = F"<RESET>{time.time()}"
2068 t.cxr_data.asset_id=0
2069
2070 return {'FINISHED'}
2071
2072 class CXR_COMPILE_MATERIAL(bpy.types.Operator):
2073 bl_idname="convexer.matcomp"
2074 bl_label="Recompile Material"
2075
2076 def execute(_,context):
2077 active_obj = bpy.context.active_object
2078 active_mat = active_obj.active_material
2079
2080 #TODO: reduce code dupe (L1663)
2081 for pair in compile_material(active_mat):
2082 decl = pair[0]
2083 pdef = pair[1]
2084 prop = pair[2]
2085
2086 if isinstance(prop,bpy.types.Image):
2087 flags = 0
2088 if 'flags' in pdef: flags = pdef['flags']
2089 prop.cxr_data.flags = flags
2090
2091 compile_image( prop )
2092
2093 settings = bpy.context.scene.cxr_data
2094 with open(F'{settings.subdir}/cfg/convexer_mat_update.cfg','w') as o:
2095 o.write(F'mat_reloadmaterial {asset_name(active_mat)}')
2096
2097 # TODO: Move this
2098 with open(F'{settings.subdir}/cfg/convexer.cfg','w') as o:
2099 o.write('sv_cheats 1\n')
2100 o.write('mp_warmup_pausetimer 1\n')
2101 o.write('bot_kick\n')
2102 o.write('alias cxr_reload "exec convexer_mat_update"\n')
2103
2104 return {'FINISHED'}
2105
2106 # Convexer panels
2107 # ------------------------------------------------------------------------------
2108
2109 # Helper buttons for 3d toolbox view
2110 #
2111 class CXR_VIEW3D( bpy.types.Panel ):
2112 bl_idname = "VIEW3D_PT_convexer"
2113 bl_label = "Convexer"
2114 bl_space_type = 'VIEW_3D'
2115 bl_region_type = 'UI'
2116 bl_category = "Convexer"
2117
2118 def draw(_, context):
2119 layout = _.layout
2120
2121 active_object = context.object
2122 if active_object == None: return
2123
2124 purpose = cxr_object_purpose( active_object )
2125
2126 if purpose[0] == None or purpose[1] == None:
2127 usage_str = "No purpose"
2128 else:
2129 if purpose[1] == 'model':
2130 usage_str = F'mesh in {asset_name( purpose[0] )}.mdl'
2131 else:
2132 usage_str = F'{purpose[1]} in {purpose[0].name}'
2133
2134 layout.label(text=F"Currently editing:")
2135 box = layout.box()
2136 box.label(text=usage_str)
2137
2138 if purpose[1] == 'brush' or purpose[1] == 'brush_entity':
2139 row = layout.row()
2140 row.scale_y = 2
2141 row.operator("convexer.preview")
2142
2143 row = layout.row()
2144 row.scale_y = 2
2145 row.operator("convexer.reset")
2146
2147 # Main scene properties interface, where all the settings go
2148 #
2149 class CXR_INTERFACE(bpy.types.Panel):
2150 bl_label="Convexer"
2151 bl_idname="SCENE_PT_convexer"
2152 bl_space_type='PROPERTIES'
2153 bl_region_type='WINDOW'
2154 bl_context="scene"
2155
2156 def draw(_,context):
2157 if CXR_GNU_LINUX==1:
2158 _.layout.operator("convexer.reload")
2159 _.layout.operator("convexer.dev_test")
2160
2161 _.layout.operator("convexer.hash_reset")
2162 settings = context.scene.cxr_data
2163
2164 _.layout.prop(settings, "scale_factor")
2165 _.layout.prop(settings, "skybox_scale_factor")
2166 _.layout.prop(settings, "skyname" )
2167 _.layout.prop(settings, "lightmap_scale")
2168 _.layout.prop(settings, "light_scale" )
2169 _.layout.prop(settings, "image_quality" )
2170
2171 box = _.layout.box()
2172
2173 box.prop(settings, "project_name")
2174 box.prop(settings, "subdir")
2175
2176 box = _.layout.box()
2177 box.operator("convexer.detect_compilers")
2178 box.prop(settings, "exe_studiomdl")
2179 box.prop(settings, "exe_vbsp")
2180 box.prop(settings, "opt_vbsp")
2181
2182 box.prop(settings, "exe_vvis")
2183 box.prop(settings, "opt_vvis")
2184
2185 box.prop(settings, "exe_vrad")
2186 box.prop(settings, "opt_vrad")
2187
2188 box = box.box()
2189 row = box.row()
2190 row.prop(settings,"comp_vmf")
2191 row.prop(settings,"comp_textures")
2192 row.prop(settings,"comp_models")
2193 row.prop(settings,"comp_compile")
2194 row.prop(settings,"comp_pack")
2195
2196 text = "Compile" if CXR_COMPILER_CHAIN.TIMER == None else "Cancel"
2197 row = box.row()
2198 row.scale_y = 3
2199 row.operator("convexer.chain", text=text)
2200
2201 row = box.row()
2202 row.scale_y = 2
2203 row.operator("convexer.reset")
2204 if CXR_COMPILER_CHAIN.TIMER != None:
2205 row.enabled = False
2206
2207 class CXR_MATERIAL_PANEL(bpy.types.Panel):
2208 bl_label="VMT Properties"
2209 bl_idname="SCENE_PT_convexer_vmt"
2210 bl_space_type='PROPERTIES'
2211 bl_region_type='WINDOW'
2212 bl_context="material"
2213
2214 def draw(_,context):
2215 active_object = bpy.context.active_object
2216 if active_object == None: return
2217
2218 active_material = active_object.active_material
2219 if active_material == None: return
2220
2221 properties = active_material.cxr_data
2222 info = material_info( active_material )
2223
2224 _.layout.label(text=F"{info['name']} @{info['res'][0]}x{info['res'][1]}")
2225 row = _.layout.row()
2226 row.prop( properties, "shader" )
2227 row.operator( "convexer.matcomp" )
2228
2229 #for xk in info: _.layout.label(text=F"{xk}:={info[xk]}")
2230
2231 def _mtex( name, img, uiParent ):
2232 nonlocal properties
2233
2234 box = uiParent.box()
2235 box.label( text=F'{name} "{img.filepath}"' )
2236 def _p2( x ):
2237 if ((x & (x - 1)) == 0):
2238 return x
2239 closest = 0
2240 closest_diff = 10000000
2241 for i in range(16):
2242 dist = abs((1 << i)-x)
2243 if dist < closest_diff:
2244 closest_diff = dist
2245 closest = i
2246 real = 1 << closest
2247 if x > real:
2248 return 1 << (closest+1)
2249 if x < real:
2250 return 1 << (closest-1)
2251 return x
2252
2253 if img is not None:
2254 row = box.row()
2255 row.prop( img.cxr_data, "export_res" )
2256 row.prop( img.cxr_data, "fmt" )
2257
2258 row = box.row()
2259 row.prop( img.cxr_data, "mipmap" )
2260 row.prop( img.cxr_data, "lod" )
2261 row.prop( img.cxr_data, "clamp" )
2262
2263 img.cxr_data.export_res[0] = _p2( img.cxr_data.export_res[0] )
2264 img.cxr_data.export_res[1] = _p2( img.cxr_data.export_res[1] )
2265
2266 def _mview( layer, uiParent ):
2267 nonlocal properties
2268
2269 for decl in layer:
2270 if isinstance(layer[decl],dict): # $property definition
2271 pdef = layer[decl]
2272 ptype = pdef['type']
2273
2274 thisnode = uiParent
2275 expandview = True
2276 drawthis = True
2277
2278 if ('shaders' in pdef) and \
2279 (properties.shader not in pdef['shaders']):
2280 continue
2281
2282 if ptype == 'intrinsic':
2283 if decl not in info:
2284 drawthis = False
2285
2286 if drawthis:
2287 for ch in pdef:
2288 if isinstance(pdef[ch],dict):
2289 if ptype == 'ui' or ptype == 'intrinsic':
2290 pass
2291 elif getattr(properties,decl) == pdef['default']:
2292 expandview = False
2293
2294 thisnode = uiParent.box()
2295 break
2296
2297 if ptype == 'ui':
2298 thisnode.label( text=decl )
2299 elif ptype == 'intrinsic':
2300 if isinstance(info[decl], bpy.types.Image):
2301 _mtex( decl, info[decl], thisnode )
2302 else:
2303 # hidden intrinsic value.
2304 # Means its a float array or something not an image
2305 thisnode.label(text=F"-- hidden intrinsic '{decl}' --")
2306 else:
2307 thisnode.prop(properties,decl)
2308 if expandview: _mview(pdef,thisnode)
2309
2310 _mview( cxr_shader_params, _.layout )
2311
2312 def cxr_entity_changeclass(_,context):
2313 active_object = context.active_object
2314
2315 # Create ID properties
2316 entdef = None
2317 classname = cxr_custom_class(active_object)
2318
2319 if classname in cxr_entities:
2320 entdef = cxr_entities[classname]
2321
2322 kvs = entdef['keyvalues']
2323 if callable(kvs): kvs = kvs( {'object': active_object} )
2324
2325 for k in kvs:
2326 kv = kvs[k]
2327 key = F'cxrkv_{k}'
2328
2329 if callable(kv) or not isinstance(kv,dict): continue
2330
2331 if key not in active_object:
2332 active_object[key] = kv['default']
2333 id_prop = active_object.id_properties_ui(key)
2334 id_prop.update(default=kv['default'])
2335
2336 class CXR_ENTITY_PANEL(bpy.types.Panel):
2337 bl_label="Entity Config"
2338 bl_idname="SCENE_PT_convexer_entity"
2339 bl_space_type='PROPERTIES'
2340 bl_region_type='WINDOW'
2341 bl_context="object"
2342
2343 def draw(_,context):
2344 active_object = bpy.context.active_object
2345
2346 if active_object == None: return
2347
2348 default_context = {
2349 "scale": bpy.context.scene.cxr_data.scale_factor,
2350 "offset": (0,0,0)
2351 }
2352
2353 ecn = cxr_intrinsic_classname( active_object )
2354 classname = cxr_custom_class( active_object )
2355
2356 if ecn == None:
2357 if active_object.type == 'MESH':
2358 _.layout.prop( active_object.cxr_data, 'brushclass' )
2359 else: _.layout.prop( active_object.cxr_data, 'classname' )
2360
2361 _.layout.prop( active_object.cxr_data, 'visgroup' )
2362 _.layout.prop( active_object.cxr_data, 'lightmap_override' )
2363
2364 if classname == 'NONE':
2365 return
2366 else:
2367 _.layout.label(text=F"<implementation defined ({ecn})>")
2368 _.layout.enabled=False
2369 classname = ecn
2370
2371 kvs = cxr_entity_keyvalues( {
2372 "object": active_object,
2373 "transform": default_context,
2374 "classname": classname
2375 })
2376
2377 if kvs != None:
2378 for kv in kvs:
2379 if kv[1]:
2380 _.layout.prop( active_object, F'["cxrkv_{kv[0]}"]', text=kv[0])
2381 else:
2382 row = _.layout.row()
2383 row.enabled = False
2384 row.label( text=F'{kv[0]}: {repr(kv[2])}' )
2385 else:
2386 _.layout.label( text=F"ERROR: NO CLASS DEFINITION" )
2387
2388 class CXR_LIGHT_PANEL(bpy.types.Panel):
2389 bl_label = "Source Settings"
2390 bl_idname = "LIGHT_PT_cxr"
2391 bl_space_type = 'PROPERTIES'
2392 bl_region_type = 'WINDOW'
2393 bl_context = "data"
2394
2395 def draw(self, context):
2396 layout = self.layout
2397 scene = context.scene
2398
2399 active_object = bpy.context.active_object
2400 if active_object == None: return
2401
2402 if active_object.type == 'LIGHT' or \
2403 active_object.type == 'LIGHT_PROBE':
2404
2405 properties = active_object.data.cxr_data
2406
2407 if active_object.type == 'LIGHT':
2408 layout.prop( properties, "realtime" )
2409 elif active_object.type == 'LIGHT_PROBE':
2410 layout.prop( properties, "size" )
2411
2412 class CXR_COLLECTION_PANEL(bpy.types.Panel):
2413 bl_label = "Source Settings"
2414 bl_idname = "COL_PT_cxr"
2415 bl_space_type = 'PROPERTIES'
2416 bl_region_type = 'WINDOW'
2417 bl_context = "collection"
2418
2419 def draw(self, context):
2420 layout = self.layout
2421 scene = context.scene
2422
2423 active_collection = bpy.context.collection
2424
2425 if active_collection != None:
2426 layout.prop( active_collection.cxr_data, "shadow_caster" )
2427 layout.prop( active_collection.cxr_data, "texture_shadows" )
2428 layout.prop( active_collection.cxr_data, "preserve_order" )
2429 layout.prop( active_collection.cxr_data, "surfaceprop" )
2430 layout.prop( active_collection.cxr_data, "visgroup" )
2431
2432 # Settings groups
2433 # ------------------------------------------------------------------------------
2434
2435 class CXR_IMAGE_SETTINGS(bpy.types.PropertyGroup):
2436 export_res: bpy.props.IntVectorProperty(
2437 name="",
2438 description="Texture Export Resolution",
2439 default=(512,512),
2440 min=1,
2441 max=4096,
2442 size=2)
2443
2444 fmt: bpy.props.EnumProperty(
2445 name="Format",
2446 items = [
2447 ('DXT1', "DXT1", "BC1 compressed", '', 0),
2448 ('DXT5', "DXT5", "BC3 compressed", '', 1),
2449 ('RGB', "RGB", "Uncompressed", '', 2),
2450 ('RGBA', "RGBA", "Uncompressed (with alpha)", '', 3)
2451 ],
2452 description="Image format",
2453 default=0)
2454
2455 last_hash: bpy.props.StringProperty( name="" )
2456 asset_id: bpy.props.IntProperty(name="intl_assetid",default=0)
2457
2458 mipmap: bpy.props.BoolProperty(name="MIP",default=True)
2459 lod: bpy.props.BoolProperty(name="LOD",default=True)
2460 clamp: bpy.props.BoolProperty(name="CLAMP",default=False)
2461 flags: bpy.props.IntProperty(name="flags",default=0)
2462
2463 class CXR_LIGHT_SETTINGS(bpy.types.PropertyGroup):
2464 realtime: bpy.props.BoolProperty(name="Realtime Light", default=True)
2465
2466 class CXR_CUBEMAP_SETTINGS(bpy.types.PropertyGroup):
2467 size: bpy.props.EnumProperty(
2468 name="Resolution",
2469 items = [
2470 ('1',"1x1",'','',0),
2471 ('2',"2x2",'','',1),
2472 ('3',"4x4",'','',2),
2473 ('4',"8x8",'','',3),
2474 ('5',"16x16",'','',4),
2475 ('6',"32x32",'','',5),
2476 ('7',"64x64",'','',6),
2477 ('8',"128x128",'','',7),
2478 ('9',"256x256",'','',8)
2479 ],
2480 description="Texture resolution",
2481 default=7)
2482
2483 class CXR_ENTITY_SETTINGS(bpy.types.PropertyGroup):
2484 entity: bpy.props.BoolProperty(name="")
2485
2486 enum_pointents = [('NONE',"None","")]
2487 enum_brushents = [('NONE',"None","")]
2488
2489 for classname in cxr_entities:
2490 entdef = cxr_entities[classname]
2491 if 'allow' in entdef:
2492 itm = [(classname, classname, "")]
2493 if 'EMPTY' in entdef['allow']: enum_pointents += itm
2494 else: enum_brushents += itm
2495
2496 classname: bpy.props.EnumProperty(items=enum_pointents, name="Class", \
2497 update=cxr_entity_changeclass, default='NONE' )
2498
2499 brushclass: bpy.props.EnumProperty(items=enum_brushents, name="Class", \
2500 update=cxr_entity_changeclass, default='NONE' )
2501
2502 enum_classes = [('0',"None","")]
2503 for i, vg in enumerate(cxr_visgroups):
2504 enum_classes += [(str(i+1),vg,"")]
2505 visgroup: bpy.props.EnumProperty(name="visgroup",items=enum_classes,default=0)
2506 lightmap_override: bpy.props.IntProperty(name="Lightmap Override",default=0)
2507
2508 class CXR_MODEL_SETTINGS(bpy.types.PropertyGroup):
2509 last_hash: bpy.props.StringProperty( name="" )
2510 asset_id: bpy.props.IntProperty(name="vmf_settings",default=0)
2511 shadow_caster: bpy.props.BoolProperty( name="Shadow caster", default=True )
2512 texture_shadows: bpy.props.BoolProperty( name="Texture Shadows", default=False )
2513 preserve_order: bpy.props.BoolProperty( name="Preserve Order", default=False )
2514 surfaceprop: bpy.props.StringProperty( name="Suface prop",default="default" )
2515
2516 enum_classes = [('0',"None","")]
2517 for i, vg in enumerate(cxr_visgroups):
2518 enum_classes += [(str(i+1),vg,"")]
2519 visgroup: bpy.props.EnumProperty(name="visgroup",items=enum_classes,default=0)
2520
2521 class CXR_SCENE_SETTINGS(bpy.types.PropertyGroup):
2522 project_name: bpy.props.StringProperty( name="Project Name" )
2523 subdir: bpy.props.StringProperty( name="../csgo/ folder" )
2524
2525 exe_studiomdl: bpy.props.StringProperty( name="studiomdl" )
2526 exe_vbsp: bpy.props.StringProperty( name="vbsp" )
2527 opt_vbsp: bpy.props.StringProperty( name="args" )
2528 exe_vvis: bpy.props.StringProperty( name="vvis" )
2529 opt_vvis: bpy.props.StringProperty( name="args" )
2530 exe_vrad: bpy.props.StringProperty( name="vrad" )
2531 opt_vrad: bpy.props.StringProperty( name="args", \
2532 default="-reflectivityScale 0.35 -aoscale 1.4 -final -textureshadows -hdr -StaticPropLighting -StaticPropPolys" )
2533
2534 scale_factor: bpy.props.FloatProperty( name="VMF Scale factor", \
2535 default=32.0,min=1.0)
2536 skybox_scale_factor: bpy.props.FloatProperty( name="Sky Scale factor", \
2537 default=1.0,min=0.01)
2538 skyname: bpy.props.StringProperty(name="Skyname",default="sky_csgo_night02b")
2539 skybox_offset: bpy.props.FloatProperty(name="Sky offset",default=-4096.0)
2540 light_scale: bpy.props.FloatProperty(name="Light Scale",default=1.0/5.0)
2541 include_names: bpy.props.BoolProperty(name="Append original file names",\
2542 default=True)
2543 lightmap_scale: bpy.props.IntProperty(name="Global Lightmap Scale",\
2544 default=12)
2545 image_quality: bpy.props.IntProperty(name="Texture Quality (0-18)",\
2546 default=8, min=0, max=18 )
2547
2548 comp_vmf: bpy.props.BoolProperty(name="VMF",default=True)
2549 comp_models: bpy.props.BoolProperty(name="Models",default=True)
2550 comp_textures: bpy.props.BoolProperty(name="Textures",default=True)
2551 comp_compile: bpy.props.BoolProperty(name="Compile",default=True)
2552 comp_pack: bpy.props.BoolProperty(name="Pack",default=False)
2553
2554 classes = [ CXR_RELOAD, CXR_DEV_OPERATOR, CXR_INTERFACE, \
2555 CXR_MATERIAL_PANEL, CXR_IMAGE_SETTINGS,\
2556 CXR_MODEL_SETTINGS, CXR_ENTITY_SETTINGS, CXR_CUBEMAP_SETTINGS,\
2557 CXR_LIGHT_SETTINGS, CXR_SCENE_SETTINGS, CXR_DETECT_COMPILERS,\
2558 CXR_ENTITY_PANEL, CXR_LIGHT_PANEL, CXR_PREVIEW_OPERATOR,\
2559 CXR_VIEW3D, CXR_COMPILER_CHAIN, CXR_RESET_HASHES,\
2560 CXR_COMPILE_MATERIAL, CXR_COLLECTION_PANEL, CXR_RESET ]
2561
2562 vmt_param_dynamic_class = None
2563
2564 def register():
2565 global cxr_view_draw_handler, vmt_param_dynamic_class, cxr_ui_handler
2566
2567 for c in classes:
2568 bpy.utils.register_class(c)
2569
2570 # Build dynamic VMT properties class defined by cxr_shader_params
2571 annotations_dict = {}
2572
2573 def _dvmt_propogate(layer):
2574 nonlocal annotations_dict
2575
2576 for decl in layer:
2577 if isinstance(layer[decl],dict): # $property definition
2578 pdef = layer[decl]
2579
2580 prop = None
2581 if pdef['type'] == 'bool':
2582 prop = bpy.props.BoolProperty(\
2583 name = pdef['name'],\
2584 default = pdef['default'])
2585
2586 elif pdef['type'] == 'float':
2587 prop = bpy.props.FloatProperty(\
2588 name = pdef['name'],\
2589 default = pdef['default'])
2590
2591 elif pdef['type'] == 'vector':
2592 if 'subtype' in pdef:
2593 prop = bpy.props.FloatVectorProperty(\
2594 name = pdef['name'],\
2595 subtype = pdef['subtype'],\
2596 default = pdef['default'],\
2597 size = len(pdef['default']))
2598 else:
2599 prop = bpy.props.FloatVectorProperty(\
2600 name = pdef['name'],\
2601 default = pdef['default'],\
2602 size = len(pdef['default']))
2603
2604 elif pdef['type'] == 'string':
2605 prop = bpy.props.StringProperty(\
2606 name = pdef['name'],\
2607 default = pdef['default'])
2608
2609 elif pdef['type'] == 'enum':
2610 prop = bpy.props.EnumProperty(\
2611 name = pdef['name'],\
2612 items = pdef['items'],\
2613 default = pdef['default'])
2614
2615 if prop != None:
2616 annotations_dict[decl] = prop
2617
2618 # Recurse into sub-definitions
2619 _dvmt_propogate(pdef)
2620
2621 annotations_dict["shader"] = bpy.props.EnumProperty(\
2622 name = "Shader",\
2623 items = [( _,\
2624 cxr_shaders[_]["name"],\
2625 '') for _ in cxr_shaders],\
2626 default = next(iter(cxr_shaders)))
2627
2628 annotations_dict["asset_id"] = bpy.props.IntProperty(name="intl_assetid",\
2629 default=0)
2630
2631 _dvmt_propogate( cxr_shader_params )
2632 vmt_param_dynamic_class = type(
2633 "CXR_VMT_DYNAMIC",
2634 (bpy.types.PropertyGroup,),{
2635 "__annotations__": annotations_dict
2636 },
2637 )
2638
2639 bpy.utils.register_class( vmt_param_dynamic_class )
2640
2641 # Pointer types
2642 bpy.types.Material.cxr_data = \
2643 bpy.props.PointerProperty(type=vmt_param_dynamic_class)
2644 bpy.types.Image.cxr_data = \
2645 bpy.props.PointerProperty(type=CXR_IMAGE_SETTINGS)
2646 bpy.types.Object.cxr_data = \
2647 bpy.props.PointerProperty(type=CXR_ENTITY_SETTINGS)
2648 bpy.types.Collection.cxr_data = \
2649 bpy.props.PointerProperty(type=CXR_MODEL_SETTINGS)
2650 bpy.types.Light.cxr_data = \
2651 bpy.props.PointerProperty(type=CXR_LIGHT_SETTINGS)
2652 bpy.types.LightProbe.cxr_data = \
2653 bpy.props.PointerProperty(type=CXR_CUBEMAP_SETTINGS)
2654 bpy.types.Scene.cxr_data = \
2655 bpy.props.PointerProperty(type=CXR_SCENE_SETTINGS)
2656
2657 # CXR Scene settings
2658
2659 # GPU / callbacks
2660 cxr_view_draw_handler = bpy.types.SpaceView3D.draw_handler_add(\
2661 cxr_draw,(),'WINDOW','POST_VIEW')
2662
2663 cxr_ui_handler = bpy.types.SpaceView3D.draw_handler_add(\
2664 cxr_ui,(None,None),'WINDOW','POST_PIXEL')
2665
2666 bpy.app.handlers.load_post.append(cxr_on_load)
2667 bpy.app.handlers.depsgraph_update_post.append(cxr_dgraph_update)
2668
2669 def unregister():
2670 global cxr_view_draw_handler, vmt_param_dynamic_class, cxr_ui_handler
2671
2672 bpy.utils.unregister_class( vmt_param_dynamic_class )
2673 for c in classes:
2674 bpy.utils.unregister_class(c)
2675
2676 bpy.app.handlers.depsgraph_update_post.remove(cxr_dgraph_update)
2677 bpy.app.handlers.load_post.remove(cxr_on_load)
2678
2679 bpy.types.SpaceView3D.draw_handler_remove(cxr_view_draw_handler,'WINDOW')
2680 bpy.types.SpaceView3D.draw_handler_remove(cxr_ui_handler,'WINDOW')