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