added shader error info
[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, str ):
990 return val
991 else:
992 for shader_variant in val:
993 if shader_variant[0] == mat.cxr_data.shader:
994 return shader_variant[1]
995
996 # Find rootnodes
997 if node == None:
998 _graph_read.extracted = []
999
1000 for node_idname in node_def:
1001 for n in mat.node_tree.nodes:
1002 if n.bl_idname == node_idname:
1003 node_def = node_def[node_idname]
1004 node = n
1005 break
1006
1007 for link in node_def:
1008 if isinstance( node_def[link], dict ):
1009 inputt = node.inputs[link]
1010 inputt_def = node_def[link]
1011
1012 if inputt.is_linked:
1013
1014 # look for definitions for the connected node type
1015 con = inputt.links[0].from_node
1016
1017 for node_idname in inputt_def:
1018 if con.bl_idname == node_idname:
1019 con_def = inputt_def[ node_idname ]
1020 _graph_read( con_def, con, depth+1 )
1021
1022 # No definition found! :(
1023 # TODO: Make a warning for this?
1024
1025 else:
1026 if "default" in inputt_def:
1027 prop = _variant_apply( inputt_def['default'] )
1028 info[prop] = inputt.default_value
1029 else:
1030 prop = _variant_apply( node_def[link] )
1031 info[prop] = getattr(node,link)
1032
1033 _graph_read(cxr_graph_mapping)
1034
1035 if "$basetexture" in info:
1036 export_res = info['$basetexture'].cxr_data.export_res
1037 info['res'] = (export_res[0], export_res[1])
1038
1039 return info
1040
1041 def vec3_min( a, b ):
1042 return mathutils.Vector((min(a[0],b[0]),min(a[1],b[1]),min(a[2],b[2])))
1043 def vec3_max( a, b ):
1044 return mathutils.Vector((max(a[0],b[0]),max(a[1],b[1]),max(a[2],b[2])))
1045
1046 def cxr_collection_center(collection, transform):
1047 BIG=999999999
1048 bounds_min = mathutils.Vector((BIG,BIG,BIG))
1049 bounds_max = mathutils.Vector((-BIG,-BIG,-BIG))
1050
1051 for obj in collection.objects:
1052 if obj.type == 'MESH':
1053 corners = [ mathutils.Vector(c) for c in obj.bound_box ]
1054
1055 for corner in [ obj.matrix_world@c for c in corners ]:
1056 bounds_min = vec3_min( bounds_min, corner )
1057 bounds_max = vec3_max( bounds_max, corner )
1058
1059 center = (bounds_min + bounds_max) / 2.0
1060
1061 origin = mathutils.Vector((-center[1],center[0],center[2]))
1062 origin *= transform['scale']
1063
1064 return origin
1065
1066 # Prepares Scene into dictionary format
1067 #
1068 def cxr_scene_collect():
1069 context = bpy.context
1070
1071 # Make sure all of our asset types have a unique ID
1072 def _uid_prepare(objtype):
1073 used_ids = [0]
1074 to_generate = []
1075 id_max = 0
1076 for o in objtype:
1077 vs = o.cxr_data
1078 if vs.asset_id in used_ids:
1079 to_generate+=[vs]
1080 else:
1081 id_max = max(id_max,vs.asset_id)
1082 used_ids+=[vs.asset_id]
1083 for vs in to_generate:
1084 id_max += 1
1085 vs.asset_id = id_max
1086 _uid_prepare(bpy.data.materials)
1087 _uid_prepare(bpy.data.images)
1088 _uid_prepare(bpy.data.collections)
1089
1090 sceneinfo = {
1091 "entities": [], # Everything with a classname
1092 "geo": [], # All meshes without a classname
1093 "heros": [] # Collections prefixed with mdl_
1094 }
1095
1096 def _collect(collection,transform):
1097 nonlocal sceneinfo
1098
1099 purpose = cxr_collection_purpose( collection )
1100 if purpose == None: return
1101 if purpose == 'model':
1102 sceneinfo['entities'] += [{
1103 "object": collection,
1104 "classname": "prop_static",
1105 "transform": transform,
1106 "origin": cxr_collection_center( collection, transform )
1107 }]
1108
1109 sceneinfo['heros'] += [{
1110 "collection": collection,
1111 "transform": transform,
1112 "origin": cxr_collection_center( collection, transform )
1113 }]
1114 return
1115
1116 for obj in collection.objects:
1117 if obj.hide_get(): continue
1118
1119 classname = cxr_classname( obj )
1120
1121 if classname != None:
1122 sceneinfo['entities'] += [{
1123 "object": obj,
1124 "classname": classname,
1125 "transform": transform
1126 }]
1127 elif obj.type == 'MESH':
1128 sceneinfo['geo'] += [{
1129 "object": obj,
1130 "transform": transform
1131 }]
1132
1133 for c in collection.children:
1134 _collect( c, transform )
1135
1136 transform_main = {
1137 "scale": context.scene.cxr_data.scale_factor,
1138 "offset": (0,0,0)
1139 }
1140
1141 transform_sky = {
1142 "scale": context.scene.cxr_data.skybox_scale_factor,
1143 "offset": (0,0,context.scene.cxr_data.skybox_offset )
1144 }
1145
1146 if 'main' in bpy.data.collections:
1147 _collect( bpy.data.collections['main'], transform_main )
1148
1149 if 'skybox' in bpy.data.collections:
1150 _collect( bpy.data.collections['skybox'], transform_sky )
1151
1152 sceneinfo['entities'] += [{
1153 "object": None,
1154 "transform": transform_sky,
1155 "classname": "sky_camera"
1156 }]
1157
1158 return sceneinfo
1159
1160 # Write VMF out to file (JOB HANDLER)
1161 #
1162 def cxr_export_vmf(sceneinfo, output_vmf):
1163 cxr_reset_lines()
1164
1165 with vdf_structure(output_vmf) as m:
1166 print( F"Write: {output_vmf}" )
1167
1168 vmfinfo = cxr_vmf_context()
1169 vmfinfo.mapversion = 4
1170
1171 #TODO: These need to be in options...
1172 vmfinfo.skyname = bpy.context.scene.cxr_data.skyname.encode('utf-8')
1173 vmfinfo.detailvbsp = b"detail.vbsp"
1174 vmfinfo.detailmaterial = b"detail/detailsprites"
1175 vmfinfo.lightmap_scale = 12
1176
1177 vmfinfo.brush_count = 0
1178 vmfinfo.entity_count = 0
1179 vmfinfo.face_count = 0
1180
1181 visgroups = (cxr_visgroup*len(cxr_visgroups))()
1182 for i, vg in enumerate(cxr_visgroups):
1183 visgroups[i].name = vg.encode('utf-8')
1184 vmfinfo.visgroups = cast(visgroups, POINTER(cxr_visgroup))
1185 vmfinfo.visgroup_count = len(cxr_visgroups)
1186
1187 libcxr_begin_vmf.call( pointer(vmfinfo), m.fp )
1188
1189 def _buildsolid( cmd ):
1190 nonlocal m
1191
1192 print( F"{vmfinfo.brush_count} :: {cmd['object'].name}" )
1193
1194 baked = mesh_cxr_format( cmd['object'] )
1195 world = cxr_decompose_globalerr( baked )
1196
1197 if world == None:
1198 return False
1199
1200 vmfinfo.scale = cmd['transform']['scale']
1201
1202 offset = cmd['transform']['offset']
1203 vmfinfo.offset[0] = offset[0]
1204 vmfinfo.offset[1] = offset[1]
1205 vmfinfo.offset[2] = offset[2]
1206
1207 if cmd['object'].cxr_data.lightmap_override > 0:
1208 vmfinfo.lightmap_scale = cmd['object'].cxr_data.lightmap_override
1209 else:
1210 vmfinfo.lightmap_scale = bpy.context.scene.cxr_data.lightmap_scale
1211
1212 libcxr_push_world_vmf.call( world, pointer(vmfinfo), m.fp )
1213 libcxr_free_world.call( world )
1214
1215 return True
1216
1217 # World geometry
1218 for brush in sceneinfo['geo']:
1219 vmfinfo.visgroupid = int(brush['object'].cxr_data.visgroup)
1220 if not _buildsolid( brush ):
1221 cxr_batch_lines()
1222 scene_redraw()
1223 return False
1224 vmfinfo.visgroupid = 0
1225
1226 libcxr_vmf_begin_entities.call(pointer(vmfinfo), m.fp)
1227
1228 # Entities
1229 for ent in sceneinfo['entities']:
1230 obj = ent['object']
1231 ctx = ent['transform']
1232 cls = ent['classname']
1233
1234 m.node( 'entity' )
1235 m.kv( 'classname', cls )
1236
1237 kvs = cxr_entity_keyvalues( ent )
1238
1239 for kv in kvs:
1240 if isinstance(kv[2], list):
1241 m.kv( kv[0], ' '.join([str(_) for _ in kv[2]]) )
1242 else: m.kv( kv[0], str(kv[2]) )
1243
1244 if obj == None:
1245 pass
1246 elif not isinstance( obj, bpy.types.Collection ):
1247 if obj.type == 'MESH':
1248 vmfinfo.visgroupid = int(obj.cxr_data.visgroup)
1249 if not _buildsolid( ent ):
1250 cxr_batch_lines()
1251 scene_redraw()
1252 return False
1253
1254 if obj != None:
1255 m.node( 'editor' )
1256 m.kv( 'visgroupid', str(obj.cxr_data.visgroup) )
1257 m.kv( 'visgroupshown', '1' )
1258 m.kv( 'visgroupautoshown', '1' )
1259 m.edon()
1260
1261 m.edon()
1262 vmfinfo.visgroupid = 0
1263
1264 print( "Done" )
1265 return True
1266
1267 # COmpile image using NBVTF and hash it (JOB HANDLER)
1268 #
1269 def compile_image(img):
1270 if img==None:
1271 return None
1272
1273 name = asset_name(img)
1274 src_path = bpy.path.abspath(img.filepath)
1275
1276 dims = img.cxr_data.export_res
1277 fmt = {
1278 'RGBA': NBVTF_IMAGE_FORMAT_ABGR8888,
1279 'DXT1': NBVTF_IMAGE_FORMAT_DXT1,
1280 'DXT5': NBVTF_IMAGE_FORMAT_DXT5,
1281 'RGB': NBVTF_IMAGE_FORMAT_BGR888
1282 }[ img.cxr_data.fmt ]
1283
1284 mipmap = img.cxr_data.mipmap
1285 lod = img.cxr_data.lod
1286 clamp = img.cxr_data.clamp
1287 flags = img.cxr_data.flags
1288
1289 q=bpy.context.scene.cxr_data.image_quality
1290
1291 userflag_hash = F"{mipmap}.{lod}.{clamp}.{flags}"
1292 file_hash = F"{name}.{os.path.getmtime(src_path)}"
1293 comphash = F"{file_hash}.{dims[0]}.{dims[1]}.{fmt}.{userflag_hash}.{q}"
1294
1295 if img.cxr_data.last_hash != comphash:
1296 print( F"Texture update: {img.filepath}" )
1297
1298 src = src_path.encode('utf-8')
1299 dst = (asset_full_path('materials',img)+'.vtf').encode('utf-8')
1300
1301 flags_full = flags
1302
1303 # texture setting flags
1304 if not lod: flags_full |= NBVTF_TEXTUREFLAGS_NOLOD
1305 if clamp:
1306 flags_full |= NBVTF_TEXTUREFLAGS_CLAMPS
1307 flags_full |= NBVTF_TEXTUREFLAGS_CLAMPT
1308
1309 if libnbvtf_convert.call(src,dims[0],dims[1],mipmap,fmt,q,flags_full,dst):
1310 img.cxr_data.last_hash = comphash
1311
1312 return name
1313
1314 #
1315 # Compile a material to VMT format. This is quick to run, doesnt need to be a
1316 # job handler.
1317 #
1318 def compile_material(mat):
1319 info = material_info(mat)
1320 properties = mat.cxr_data
1321
1322 print( F"Compile {asset_full_path('materials',mat)}.vmt" )
1323 if properties.shader == 'Builtin':
1324 return []
1325
1326 props = []
1327
1328 # Walk the property tree
1329 def _mlayer( layer ):
1330 nonlocal properties, props
1331
1332 for decl in layer:
1333 if isinstance(layer[decl],dict): # $property definition
1334 pdef = layer[decl]
1335 ptype = pdef['type']
1336
1337 subdefines = False
1338 default = None
1339 prop = None
1340
1341 if 'shaders' in pdef and properties.shader not in pdef['shaders']:
1342 continue
1343
1344 # Group expansion (does it have subdefinitions?)
1345 for ch in pdef:
1346 if isinstance(pdef[ch],dict):
1347 subdefines = True
1348 break
1349
1350 expandview = False
1351
1352 if ptype == 'ui':
1353 expandview = True
1354 else:
1355 if ptype == 'intrinsic':
1356 if decl in info:
1357 prop = info[decl]
1358 else:
1359 prop = getattr(properties,decl)
1360 default = pdef['default']
1361
1362 if not isinstance(prop,str) and \
1363 not isinstance(prop,bpy.types.Image) and \
1364 hasattr(prop,'__getitem__'):
1365 prop = tuple([p for p in prop])
1366
1367 if prop != default:
1368 # write prop
1369 props += [(decl,pdef,prop)]
1370
1371 if subdefines:
1372 expandview = True
1373
1374 if expandview: _mlayer(pdef)
1375
1376 _mlayer( cxr_shader_params )
1377
1378 # Write the vmt
1379 with vdf_structure( F"{asset_full_path('materials',mat)}.vmt" ) as vmt:
1380 vmt.node( properties.shader )
1381 vmt.put( "// Convexer export\n" )
1382
1383 for pair in props:
1384 decl = pair[0]
1385 pdef = pair[1]
1386 prop = pair[2]
1387
1388 def _numeric(v):
1389 nonlocal pdef
1390 if 'exponent' in pdef: return str(pow( v, pdef['exponent'] ))
1391 else: return str(v)
1392
1393 if isinstance(prop,bpy.types.Image):
1394 vmt.kv( decl, asset_name(prop))
1395 elif isinstance(prop,bool):
1396 vmt.kv( decl, '1' if prop else '0' )
1397 elif isinstance(prop,str):
1398 vmt.kv( decl, prop )
1399 elif isinstance(prop,float) or isinstance(prop,int):
1400 vmt.kv( decl, _numeric(prop) )
1401 elif isinstance(prop,tuple):
1402 vmt.kv( decl, F"[{' '.join([_numeric(_) for _ in prop])}]" )
1403 else:
1404 vmt.put( F"// (cxr) unkown shader value type'{type(prop)}'" )
1405
1406 vmt.edon()
1407 return props
1408
1409 def cxr_modelsrc_vphys( mdl ):
1410 for obj in mdl.objects:
1411 if obj.name == F"{mdl.name}_phy":
1412 return obj
1413 return None
1414
1415 def cxr_export_modelsrc( mdl, origin, asset_dir, project_name, transform ):
1416 dgraph = bpy.context.evaluated_depsgraph_get()
1417
1418 # Compute hash value
1419 chash = asset_uid(mdl)+str(origin)+str(transform)
1420
1421 #for obj in mdl.objects:
1422 # if obj.type != 'MESH':
1423 # continue
1424
1425 # ev = obj.evaluated_get(dgraph).data
1426 # srcverts=[(v.co[0],v.co[1],v.co[2]) for v in ev.vertices]
1427 # srcloops=[(l.normal[0],l.normal[1],l.normal[2]) for l in ev.loops]
1428
1429 # chash=hashlib.sha224((str(srcverts)+chash).encode()).hexdigest()
1430 # chash=hashlib.sha224((str(srcloops)+chash).encode()).hexdigest()
1431
1432 # if ev.uv_layers.active != None:
1433 # uv_layer = ev.uv_layers.active.data
1434 # srcuv=[(uv.uv[0],uv.uv[1]) for uv in uv_layer]
1435 # else:
1436 # srcuv=['none']
1437
1438 # chash=hashlib.sha224((str(srcuv)+chash).encode()).hexdigest()
1439 # srcmats=[ ms.material.name for ms in obj.material_slots ]
1440 # chash=hashlib.sha224((str(srcmats)+chash).encode()).hexdigest()
1441 # transforms=[ obj.location, obj.rotation_euler, obj.scale ]
1442 # srctr=[(v[0],v[1],v[2]) for v in transforms]
1443 # chash=hashlib.sha224((str(srctr)+chash).encode()).hexdigest()
1444
1445 #if chash != mdl.cxr_data.last_hash:
1446 # mdl.cxr_data.last_hash = chash
1447 # print( F"Compile: {mdl.name}" )
1448 #else:
1449 # return True
1450
1451 bpy.ops.object.select_all(action='DESELECT')
1452
1453 # Get viewlayer
1454 def _get_layer(col,name):
1455 for c in col.children:
1456 if c.name == name:
1457 return c
1458 sub = _get_layer(c,name)
1459 if sub != None:
1460 return sub
1461 return None
1462 layer = _get_layer(bpy.context.view_layer.layer_collection,mdl.name)
1463
1464 prev_state = layer.hide_viewport
1465 layer.hide_viewport=False
1466
1467 # Collect materials to be compiled, and temp rename for export
1468 mat_dict = {}
1469
1470 vphys = None
1471 for obj in mdl.objects:
1472 if obj.name == F"{mdl.name}_phy":
1473 vphys = obj
1474 continue
1475
1476 obj.select_set(state=True)
1477 for ms in obj.material_slots:
1478 if ms.material != None:
1479 if ms.material not in mat_dict:
1480 mat_dict[ms.material] = ms.material.name
1481 ms.material.name = asset_uid(ms.material)
1482 ms.material.use_nodes = False
1483
1484 uid=asset_uid(mdl)
1485 bpy.ops.export_scene.fbx( filepath=F'{asset_dir}/{uid}_ref.fbx',\
1486 check_existing=False,
1487 use_selection=True,
1488 apply_unit_scale=False,
1489 bake_space_transform=False
1490 )
1491
1492 bpy.ops.object.select_all(action='DESELECT')
1493
1494 if vphys != None:
1495 vphys.select_set(state=True)
1496 bpy.ops.export_scene.fbx( filepath=F'{asset_dir}/{uid}_phy.fbx',\
1497 check_existing=False,
1498 use_selection=True,
1499 apply_unit_scale=False,
1500 bake_space_transform=False
1501 )
1502 bpy.ops.object.select_all(action='DESELECT')
1503
1504 # Fix material names back to original
1505 for mat in mat_dict:
1506 mat.name = mat_dict[mat]
1507 mat.use_nodes = True
1508
1509 layer.hide_viewport=prev_state
1510
1511 # Write out QC file
1512 with open(F'{asset_dir}/{uid}.qc','w') as o:
1513 o.write(F'$modelname "{project_name}/{uid}"\n')
1514 #o.write(F'$scale .32\n')
1515 o.write(F'$scale {transform["scale"]/100.0}\n')
1516 o.write(F'$body _ "{uid}_ref.fbx"\n')
1517 o.write(F'$staticprop\n')
1518 o.write(F'$origin {origin[0]:.6f} {origin[1]:.6f} {origin[2]:.6f}\n')
1519
1520 if mdl.cxr_data.preserve_order:
1521 o.write(F"$preservetriangleorder\n")
1522
1523 if mdl.cxr_data.texture_shadows:
1524 o.write(F"$casttextureshadows\n")
1525
1526 o.write(F"$surfaceprop {mdl.cxr_data.surfaceprop}\n")
1527
1528 if vphys != None:
1529 o.write(F'$collisionmodel "{uid}_phy.fbx"\n')
1530 o.write("{\n")
1531 o.write(" $concave\n")
1532 o.write("}\n")
1533
1534 o.write(F'$cdmaterials {project_name}\n')
1535 o.write(F'$sequence idle {uid}_ref.fbx\n')
1536
1537 return True
1538 #
1539 # Copy bsp file (and also lightpatch it)
1540 #
1541 def cxr_patchmap( src, dst ):
1542 libcxr_lightpatch_bsp.call( src.encode('utf-8') )
1543 shutil.copyfile( src, dst )
1544 return True
1545
1546 # Convexer operators
1547 # ------------------------------------------------------------------------------
1548
1549 # Force reload of shared libraries
1550 #
1551 class CXR_RELOAD(bpy.types.Operator):
1552 bl_idname="convexer.reload"
1553 bl_label="Reload"
1554 def execute(_,context):
1555 shared_reload()
1556 return {'FINISHED'}
1557
1558 # Reset all debugging/ui information
1559 #
1560 class CXR_RESET(bpy.types.Operator):
1561 bl_idname="convexer.reset"
1562 bl_label="Reset Convexer"
1563 def execute(_,context):
1564 cxr_reset_all()
1565 return {'FINISHED'}
1566
1567 # Used for exporting data to use with ASAN builds
1568 #
1569 class CXR_DEV_OPERATOR(bpy.types.Operator):
1570 bl_idname="convexer.dev_test"
1571 bl_label="Export development data"
1572
1573 def execute(_,context):
1574 # Prepare input data
1575 mesh_src = mesh_cxr_format(context.active_object)
1576 libcxr_write_test_data.call( pointer(mesh_src) )
1577 return {'FINISHED'}
1578
1579 # UI: Preview how the brushes will looks in 3D view
1580 #
1581 class CXR_PREVIEW_OPERATOR(bpy.types.Operator):
1582 bl_idname="convexer.preview"
1583 bl_label="Preview Brushes"
1584
1585 RUNNING = False
1586
1587 def execute(_,context):
1588 global cxr_view_mesh
1589 global cxr_view_shader, cxr_view_mesh, cxr_error_inf
1590
1591 cxr_reset_all()
1592
1593 static = _.__class__
1594
1595 mesh_src = mesh_cxr_format(context.active_object)
1596 world = cxr_decompose_globalerr( mesh_src )
1597
1598 if world == None:
1599 return {'FINISHED'}
1600
1601 # Generate preview using cxr
1602 #
1603 ptrpreview = libcxr_world_preview.call( world )
1604 preview = ptrpreview[0]
1605
1606 vertices = preview.vertices[:preview.vertex_count]
1607 vertices = [(_[0],_[1],_[2]) for _ in vertices]
1608
1609 colours = preview.colours[:preview.vertex_count]
1610 colours = [(_[0],_[1],_[2],_[3]) for _ in colours]
1611
1612 indices = preview.indices[:preview.indices_count]
1613 indices = [ (indices[i*3+0],indices[i*3+1],indices[i*3+2]) \
1614 for i in range(int(preview.indices_count/3)) ]
1615
1616 cxr_view_mesh = batch_for_shader(
1617 cxr_view_shader, 'TRIS',
1618 { "pos": vertices, "color": colours },
1619 indices = indices,
1620 )
1621
1622 libcxr_free_tri_mesh.call( ptrpreview )
1623 libcxr_free_world.call( world )
1624 cxr_batch_lines()
1625 scene_redraw()
1626
1627 return {'FINISHED'}
1628
1629 # Search for VMF compiler executables in subdirectory
1630 #
1631 class CXR_DETECT_COMPILERS(bpy.types.Operator):
1632 bl_idname="convexer.detect_compilers"
1633 bl_label="Find compilers"
1634
1635 def execute(self,context):
1636 scene = context.scene
1637 settings = scene.cxr_data
1638 subdir = settings.subdir
1639
1640 for exename in ['studiomdl','vbsp','vvis','vrad']:
1641 searchpath = os.path.normpath(F'{subdir}/../bin/{exename}.exe')
1642 if os.path.exists(searchpath):
1643 settings[F'exe_{exename}'] = searchpath
1644
1645 return {'FINISHED'}
1646
1647 def cxr_compiler_path( compiler ):
1648 settings = bpy.context.scene.cxr_data
1649 subdir = settings.subdir
1650 path = os.path.normpath(F'{subdir}/../bin/{compiler}.exe')
1651
1652 if os.path.exists( path ): return path
1653 else: return None
1654
1655 # Compatibility layer
1656 #
1657 def cxr_temp_file( fn ):
1658 if CXR_GNU_LINUX == 1:
1659 return F"/tmp/fn"
1660 else:
1661 filepath = bpy.data.filepath
1662 directory = os.path.dirname(filepath)
1663 return F"{directory}/{fn}"
1664
1665 def cxr_winepath( path ):
1666 if CXR_GNU_LINUX == 1:
1667 return 'z:'+path.replace('/','\\')
1668 else:
1669 return path
1670
1671 # Main compile function
1672 #
1673 class CXR_COMPILER_CHAIN(bpy.types.Operator):
1674 bl_idname="convexer.chain"
1675 bl_label="Compile Chain"
1676
1677 # 'static'
1678 USER_EXIT = False
1679 SUBPROC = None
1680 TIMER = None
1681 TIMER_LAST = 0.0
1682 WAIT_REDRAW = False
1683 FILE = None
1684 LOG = []
1685
1686 JOBINFO = None
1687 JOBID = 0
1688 JOBSYS = None
1689
1690 def cancel(_,context):
1691 #global cxr_jobs_batch
1692 static = _.__class__
1693 wm = context.window_manager
1694
1695 if static.SUBPROC != None:
1696 static.SUBPROC.terminate()
1697 static.SUBPROC = None
1698
1699 if static.TIMER != None:
1700 wm.event_timer_remove( static.TIMER )
1701 static.TIMER = None
1702
1703 static.FILE.close()
1704
1705 #cxr_jobs_batch = None
1706 scene_redraw()
1707 return {'FINISHED'}
1708
1709 def modal(_,context,ev):
1710 static = _.__class__
1711
1712 if ev.type == 'TIMER':
1713 global cxr_jobs_batch, cxr_error_inf
1714
1715 if static.WAIT_REDRAW:
1716 scene_redraw()
1717 return {'PASS_THROUGH'}
1718 static.WAIT_REDRAW = True
1719
1720 if static.USER_EXIT:
1721 print( "Chain USER_EXIT" )
1722 return _.cancel(context)
1723
1724 if static.SUBPROC != None:
1725 # Deal with async modes
1726 status = static.SUBPROC.poll()
1727
1728 # Cannot redirect STDOUT through here without causing
1729 # undefined behaviour due to the Blender Python specification.
1730 #
1731 # Have to write it out to a file and read it back in.
1732 #
1733
1734 with open(cxr_temp_file("convexer_compile_log.txt"),"r") as log:
1735 static.LOG = log.readlines()
1736 if status == None:
1737 return {'PASS_THROUGH'}
1738 else:
1739 #for l in static.SUBPROC.stdout:
1740 # print( F'-> {l.decode("utf-8")}',end='' )
1741 static.SUBPROC = None
1742
1743 if status != 0:
1744 print(F'Compiler () error: {status}')
1745
1746 jobn = static.JOBSYS['jobs'][static.JOBID]
1747 cxr_error_inf = ( F"{static.JOBSYS['title']} error {status}", jobn )
1748
1749 return _.cancel(context)
1750
1751 static.JOBSYS['jobs'][static.JOBID] = None
1752 cxr_jobs_update_graph( static.JOBINFO )
1753 scene_redraw()
1754 return {'PASS_THROUGH'}
1755
1756 # Compile syncronous thing
1757 for sys in static.JOBINFO:
1758 for i,target in enumerate(sys['jobs']):
1759 if target != None:
1760
1761 if callable(sys['exec']):
1762 print( F"Run (sync): {static.JOBID} @{time.time()}" )
1763
1764 if not sys['exec'](*target):
1765 print( "Job failed" )
1766 return _.cancel(context)
1767
1768 sys['jobs'][i] = None
1769 static.JOBID += 1
1770 else:
1771 # Run external executable (wine)
1772 static.SUBPROC = subprocess.Popen( target,
1773 stdout=static.FILE,\
1774 stderr=subprocess.PIPE,\
1775 cwd=sys['cwd'])
1776 static.JOBSYS = sys
1777 static.JOBID = i
1778
1779 cxr_jobs_update_graph( static.JOBINFO )
1780 scene_redraw()
1781 return {'PASS_THROUGH'}
1782
1783 # All completed
1784 print( "All jobs completed!" )
1785 #cxr_jobs_batch = None
1786 #scene_redraw()
1787 return _.cancel(context)
1788
1789 return {'PASS_THROUGH'}
1790
1791 def invoke(_,context,event):
1792 global cxr_error_inf
1793
1794 static = _.__class__
1795 wm = context.window_manager
1796
1797 if static.TIMER != None:
1798 print("Chain exiting...")
1799 static.USER_EXIT=True
1800 return {'RUNNING_MODAL'}
1801
1802 print("Launching compiler toolchain")
1803 cxr_reset_all()
1804
1805 # Run static compilation units now (collect, vmt..)
1806 filepath = bpy.data.filepath
1807 directory = os.path.dirname(filepath)
1808 settings = bpy.context.scene.cxr_data
1809
1810 asset_dir = F"{directory}/modelsrc"
1811 material_dir = F"{settings.subdir}/materials/{settings.project_name}"
1812 model_dir = F"{settings.subdir}/models/{settings.project_name}"
1813 output_vmf = F"{directory}/{settings.project_name}.vmf"
1814
1815 bsp_local = F"{directory}/{settings.project_name}.bsp"
1816 bsp_remote = F"{settings.subdir}/maps/{settings.project_name}.bsp"
1817 bsp_packed = F"{settings.subdir}/maps/{settings.project_name}_pack.bsp"
1818 packlist = F"{directory}/{settings.project_name}_assets.txt"
1819
1820 os.makedirs( asset_dir, exist_ok=True )
1821 os.makedirs( material_dir, exist_ok=True )
1822 os.makedirs( model_dir, exist_ok=True )
1823
1824 static.FILE = open(cxr_temp_file("convexer_compile_log.txt"),"w")
1825 static.LOG = []
1826
1827 sceneinfo = cxr_scene_collect()
1828 image_jobs = []
1829 qc_jobs = []
1830
1831 # Collect materials
1832 a_materials = set()
1833 for brush in sceneinfo['geo']:
1834 for ms in brush['object'].material_slots:
1835 a_materials.add( ms.material )
1836 if ms.material.cxr_data.shader == 'VertexLitGeneric':
1837 errmat = ms.material.name
1838 errnam = brush['object'].name
1839
1840 cxr_error_inf = ( "Shader error", \
1841 F"Vertex shader ({errmat}) used on model ({errnam})" )
1842
1843 print( F"Vertex shader {errmat} used on {errnam}")
1844 scene_redraw()
1845 return {'CANCELLED'}
1846
1847 a_models = set()
1848 model_jobs = []
1849 for ent in sceneinfo['entities']:
1850 if ent['object'] == None: continue
1851
1852 if ent['classname'] == 'prop_static':
1853 obj = ent['object']
1854 if isinstance(obj,bpy.types.Collection):
1855 target = obj
1856 a_models.add( target )
1857 model_jobs += [(target, ent['origin'], asset_dir, \
1858 settings.project_name, ent['transform'])]
1859 else:
1860 target = obj.instance_collection
1861 if target in a_models:
1862 continue
1863 a_models.add( target )
1864
1865 # TODO: Should take into account collection instancing offset
1866 model_jobs += [(target, [0,0,0], asset_dir, \
1867 settings.project_name, ent['transform'])]
1868
1869 elif ent['object'].type == 'MESH':
1870 for ms in ent['object'].material_slots:
1871 a_materials.add( ms.material )
1872
1873 for mdl in a_models:
1874 uid = asset_uid(mdl)
1875 qc_jobs += [F'{uid}.qc']
1876
1877 for obj in mdl.objects:
1878 for ms in obj.material_slots:
1879 a_materials.add( ms.material )
1880 if ms.material.cxr_data.shader == 'LightMappedGeneric' or \
1881 ms.material.cxr_data.shader == 'WorldVertexTransition':
1882
1883 errmat = ms.material.name
1884 errnam = obj.name
1885
1886 cxr_error_inf = ( "Shader error", \
1887 F"Lightmapped shader ({errmat}) used on model ({errnam})" )
1888
1889 print( F"Lightmapped shader {errmat} used on {errnam}")
1890 scene_redraw()
1891 return {'CANCELLED'}
1892
1893 # Collect images
1894 for mat in a_materials:
1895 for pair in compile_material(mat):
1896 decl = pair[0]
1897 pdef = pair[1]
1898 prop = pair[2]
1899
1900 if isinstance(prop,bpy.types.Image):
1901 flags = 0
1902 if 'flags' in pdef: flags = pdef['flags']
1903 if prop not in image_jobs:
1904 image_jobs += [(prop,)]
1905 prop.cxr_data.flags = flags
1906
1907 # Create packlist
1908 with open( packlist, "w" ) as fp:
1909
1910 for mat in a_materials:
1911 if mat.cxr_data.shader == 'Builtin': continue
1912 fp.write(F"{asset_path('materials',mat)}.vmt\n")
1913 fp.write(F"{cxr_winepath(asset_full_path('materials',mat))}.vmt\n")
1914
1915 for img_job in image_jobs:
1916 img = img_job[0]
1917 fp.write(F"{asset_path('materials',img)}.vtf\n")
1918 fp.write(F"{cxr_winepath(asset_full_path('materials',img))}.vtf\n")
1919
1920 for mdl in a_models:
1921 local = asset_path('models',mdl)
1922 winep = cxr_winepath(asset_full_path('models',mdl))
1923
1924 fp.write(F"{local}.vvd\n")
1925 fp.write(F"{winep}.vvd\n")
1926 fp.write(F"{local}.dx90.vtx\n")
1927 fp.write(F"{winep}.dx90.vtx\n")
1928 fp.write(F"{local}.mdl\n")
1929 fp.write(F"{winep}.mdl\n")
1930 fp.write(F"{local}.vvd\n")
1931 fp.write(F"{winep}.vvd\n")
1932
1933 if cxr_modelsrc_vphys(mdl):
1934 fp.write(F"{local}.phy\n")
1935 fp.write(F"{winep}.phy\n")
1936
1937 # Convexer jobs
1938 static.JOBID = 0
1939 static.JOBINFO = []
1940
1941 if settings.comp_vmf:
1942 static.JOBINFO += [{
1943 "title": "Convexer",
1944 "w": 20,
1945 "colour": (0.863, 0.078, 0.235,1.0),
1946 "exec": cxr_export_vmf,
1947 "jobs": [(sceneinfo,output_vmf)]
1948 }]
1949
1950 if settings.comp_textures:
1951 if len(image_jobs) > 0:
1952 static.JOBINFO += [{
1953 "title": "Textures",
1954 "w": 40,
1955 "colour": (1.000, 0.271, 0.000,1.0),
1956 "exec": compile_image,
1957 "jobs": image_jobs
1958 }]
1959
1960 game = cxr_winepath( settings.subdir )
1961 args = [ \
1962 '-game', game, settings.project_name
1963 ]
1964
1965 # FBX stage
1966 if settings.comp_models:
1967 if len(model_jobs) > 0:
1968 static.JOBINFO += [{
1969 "title": "Batches",
1970 "w": 25,
1971 "colour": (1.000, 0.647, 0.000,1.0),
1972 "exec": cxr_export_modelsrc,
1973 "jobs": model_jobs
1974 }]
1975
1976 if len(qc_jobs) > 0:
1977 static.JOBINFO += [{
1978 "title": "StudioMDL",
1979 "w": 20,
1980 "colour": (1.000, 0.843, 0.000, 1.0),
1981 "exec": "studiomdl",
1982 "jobs": [[settings[F'exe_studiomdl']] + [\
1983 '-nop4', '-game', game, qc] for qc in qc_jobs],
1984 "cwd": asset_dir
1985 }]
1986
1987 # VBSP stage
1988 if settings.comp_compile:
1989 if not settings.opt_vbsp.startswith( 'disable' ):
1990 vbsp_opt = settings.opt_vbsp.split()
1991 static.JOBINFO += [{
1992 "title": "VBSP",
1993 "w": 25,
1994 "colour": (0.678, 1.000, 0.184,1.0),
1995 "exec": "vbsp",
1996 "jobs": [[settings[F'exe_vbsp']] + vbsp_opt + args],
1997 "cwd": directory
1998 }]
1999
2000 if not settings.opt_vvis.startswith( 'disable' ):
2001 vvis_opt = settings.opt_vvis.split()
2002 static.JOBINFO += [{
2003 "title": "VVIS",
2004 "w": 25,
2005 "colour": (0.000, 1.000, 0.498,1.0),
2006 "exec": "vvis",
2007 "jobs": [[settings[F'exe_vvis']] + vvis_opt + args ],
2008 "cwd": directory
2009 }]
2010
2011 if not settings.opt_vrad.startswith( 'disable' ):
2012 vrad_opt = settings.opt_vrad.split()
2013 static.JOBINFO += [{
2014 "title": "VRAD",
2015 "w": 25,
2016 "colour": (0.125, 0.698, 0.667,1.0),
2017 "exec": "vrad",
2018 "jobs": [[settings[F'exe_vrad']] + vrad_opt + args ],
2019 "cwd": directory
2020 }]
2021
2022 static.JOBINFO += [{
2023 "title": "CXR",
2024 "w": 5,
2025 "colour": (0.118, 0.565, 1.000,1.0),
2026 "exec": cxr_patchmap,
2027 "jobs": [(bsp_local,bsp_remote)]
2028 }]
2029
2030 if settings.comp_pack:
2031 static.JOBINFO += [{
2032 "title": "Pack",
2033 "w": 5,
2034 "colour": (0.541, 0.169, 0.886,1.0),
2035 "exec": "bspzip",
2036 "jobs": [[cxr_compiler_path("bspzip"), '-addlist', \
2037 cxr_winepath(bsp_remote),
2038 cxr_winepath(packlist),
2039 cxr_winepath(bsp_packed) ]],
2040 "cwd": directory
2041 }]
2042
2043 if len(static.JOBINFO) == 0:
2044 return {'CANCELLED'}
2045
2046 static.USER_EXIT=False
2047 static.TIMER=wm.event_timer_add(0.1,window=context.window)
2048 wm.modal_handler_add(_)
2049
2050 cxr_jobs_update_graph( static.JOBINFO )
2051 scene_redraw()
2052 return {'RUNNING_MODAL'}
2053
2054 class CXR_RESET_HASHES(bpy.types.Operator):
2055 bl_idname="convexer.hash_reset"
2056 bl_label="Reset asset hashes"
2057
2058 def execute(_,context):
2059 for c in bpy.data.collections:
2060 c.cxr_data.last_hash = F"<RESET>{time.time()}"
2061 c.cxr_data.asset_id=0
2062
2063 for t in bpy.data.images:
2064 t.cxr_data.last_hash = F"<RESET>{time.time()}"
2065 t.cxr_data.asset_id=0
2066
2067 return {'FINISHED'}
2068
2069 class CXR_COMPILE_MATERIAL(bpy.types.Operator):
2070 bl_idname="convexer.matcomp"
2071 bl_label="Recompile Material"
2072
2073 def execute(_,context):
2074 active_obj = bpy.context.active_object
2075 active_mat = active_obj.active_material
2076
2077 #TODO: reduce code dupe (L1663)
2078 for pair in compile_material(active_mat):
2079 decl = pair[0]
2080 pdef = pair[1]
2081 prop = pair[2]
2082
2083 if isinstance(prop,bpy.types.Image):
2084 flags = 0
2085 if 'flags' in pdef: flags = pdef['flags']
2086 prop.cxr_data.flags = flags
2087
2088 compile_image( prop )
2089
2090 settings = bpy.context.scene.cxr_data
2091 with open(F'{settings.subdir}/cfg/convexer_mat_update.cfg','w') as o:
2092 o.write(F'mat_reloadmaterial {asset_name(active_mat)}')
2093
2094 # TODO: Move this
2095 with open(F'{settings.subdir}/cfg/convexer.cfg','w') as o:
2096 o.write('sv_cheats 1\n')
2097 o.write('mp_warmup_pausetimer 1\n')
2098 o.write('bot_kick\n')
2099 o.write('alias cxr_reload "exec convexer_mat_update"\n')
2100
2101 return {'FINISHED'}
2102
2103 # Convexer panels
2104 # ------------------------------------------------------------------------------
2105
2106 # Helper buttons for 3d toolbox view
2107 #
2108 class CXR_VIEW3D( bpy.types.Panel ):
2109 bl_idname = "VIEW3D_PT_convexer"
2110 bl_label = "Convexer"
2111 bl_space_type = 'VIEW_3D'
2112 bl_region_type = 'UI'
2113 bl_category = "Convexer"
2114
2115 def draw(_, context):
2116 layout = _.layout
2117
2118 active_object = context.object
2119 if active_object == None: return
2120
2121 purpose = cxr_object_purpose( active_object )
2122
2123 if purpose[0] == None or purpose[1] == None:
2124 usage_str = "No purpose"
2125 else:
2126 if purpose[1] == 'model':
2127 usage_str = F'mesh in {asset_name( purpose[0] )}.mdl'
2128 else:
2129 usage_str = F'{purpose[1]} in {purpose[0].name}'
2130
2131 layout.label(text=F"Currently editing:")
2132 box = layout.box()
2133 box.label(text=usage_str)
2134
2135 if purpose[1] == 'brush' or purpose[1] == 'brush_entity':
2136 row = layout.row()
2137 row.scale_y = 2
2138 row.operator("convexer.preview")
2139
2140 row = layout.row()
2141 row.scale_y = 2
2142 row.operator("convexer.reset")
2143
2144 # Main scene properties interface, where all the settings go
2145 #
2146 class CXR_INTERFACE(bpy.types.Panel):
2147 bl_label="Convexer"
2148 bl_idname="SCENE_PT_convexer"
2149 bl_space_type='PROPERTIES'
2150 bl_region_type='WINDOW'
2151 bl_context="scene"
2152
2153 def draw(_,context):
2154 if CXR_GNU_LINUX==1:
2155 _.layout.operator("convexer.reload")
2156 _.layout.operator("convexer.dev_test")
2157
2158 _.layout.operator("convexer.hash_reset")
2159 settings = context.scene.cxr_data
2160
2161 _.layout.prop(settings, "scale_factor")
2162 _.layout.prop(settings, "skybox_scale_factor")
2163 _.layout.prop(settings, "skyname" )
2164 _.layout.prop(settings, "lightmap_scale")
2165 _.layout.prop(settings, "light_scale" )
2166 _.layout.prop(settings, "image_quality" )
2167
2168 box = _.layout.box()
2169
2170 box.prop(settings, "project_name")
2171 box.prop(settings, "subdir")
2172
2173 box = _.layout.box()
2174 box.operator("convexer.detect_compilers")
2175 box.prop(settings, "exe_studiomdl")
2176 box.prop(settings, "exe_vbsp")
2177 box.prop(settings, "opt_vbsp")
2178
2179 box.prop(settings, "exe_vvis")
2180 box.prop(settings, "opt_vvis")
2181
2182 box.prop(settings, "exe_vrad")
2183 box.prop(settings, "opt_vrad")
2184
2185 box = box.box()
2186 row = box.row()
2187 row.prop(settings,"comp_vmf")
2188 row.prop(settings,"comp_textures")
2189 row.prop(settings,"comp_models")
2190 row.prop(settings,"comp_compile")
2191 row.prop(settings,"comp_pack")
2192
2193 text = "Compile" if CXR_COMPILER_CHAIN.TIMER == None else "Cancel"
2194 row = box.row()
2195 row.scale_y = 3
2196 row.operator("convexer.chain", text=text)
2197
2198 row = box.row()
2199 row.scale_y = 2
2200 row.operator("convexer.reset")
2201 if CXR_COMPILER_CHAIN.TIMER != None:
2202 row.enabled = False
2203
2204 class CXR_MATERIAL_PANEL(bpy.types.Panel):
2205 bl_label="VMT Properties"
2206 bl_idname="SCENE_PT_convexer_vmt"
2207 bl_space_type='PROPERTIES'
2208 bl_region_type='WINDOW'
2209 bl_context="material"
2210
2211 def draw(_,context):
2212 active_object = bpy.context.active_object
2213 if active_object == None: return
2214
2215 active_material = active_object.active_material
2216 if active_material == None: return
2217
2218 properties = active_material.cxr_data
2219 info = material_info( active_material )
2220
2221 _.layout.label(text=F"{info['name']} @{info['res'][0]}x{info['res'][1]}")
2222 row = _.layout.row()
2223 row.prop( properties, "shader" )
2224 row.operator( "convexer.matcomp" )
2225
2226 #for xk in info: _.layout.label(text=F"{xk}:={info[xk]}")
2227
2228 def _mtex( name, img, uiParent ):
2229 nonlocal properties
2230
2231 box = uiParent.box()
2232 box.label( text=F'{name} "{img.filepath}"' )
2233 def _p2( x ):
2234 if ((x & (x - 1)) == 0):
2235 return x
2236 closest = 0
2237 closest_diff = 10000000
2238 for i in range(16):
2239 dist = abs((1 << i)-x)
2240 if dist < closest_diff:
2241 closest_diff = dist
2242 closest = i
2243 real = 1 << closest
2244 if x > real:
2245 return 1 << (closest+1)
2246 if x < real:
2247 return 1 << (closest-1)
2248 return x
2249
2250 if img is not None:
2251 row = box.row()
2252 row.prop( img.cxr_data, "export_res" )
2253 row.prop( img.cxr_data, "fmt" )
2254
2255 row = box.row()
2256 row.prop( img.cxr_data, "mipmap" )
2257 row.prop( img.cxr_data, "lod" )
2258 row.prop( img.cxr_data, "clamp" )
2259
2260 img.cxr_data.export_res[0] = _p2( img.cxr_data.export_res[0] )
2261 img.cxr_data.export_res[1] = _p2( img.cxr_data.export_res[1] )
2262
2263 def _mview( layer, uiParent ):
2264 nonlocal properties
2265
2266 for decl in layer:
2267 if isinstance(layer[decl],dict): # $property definition
2268 pdef = layer[decl]
2269 ptype = pdef['type']
2270
2271 thisnode = uiParent
2272 expandview = True
2273 drawthis = True
2274
2275 if ('shaders' in pdef) and \
2276 (properties.shader not in pdef['shaders']):
2277 continue
2278
2279 if ptype == 'intrinsic':
2280 if decl not in info:
2281 drawthis = False
2282
2283 if drawthis:
2284 for ch in pdef:
2285 if isinstance(pdef[ch],dict):
2286 if ptype == 'ui' or ptype == 'intrinsic':
2287 pass
2288 elif getattr(properties,decl) == pdef['default']:
2289 expandview = False
2290
2291 thisnode = uiParent.box()
2292 break
2293
2294 if ptype == 'ui':
2295 thisnode.label( text=decl )
2296 elif ptype == 'intrinsic':
2297 if isinstance(info[decl], bpy.types.Image):
2298 _mtex( decl, info[decl], thisnode )
2299 else:
2300 # hidden intrinsic value.
2301 # Means its a float array or something not an image
2302 thisnode.label(text=F"-- hidden intrinsic '{decl}' --")
2303 else:
2304 thisnode.prop(properties,decl)
2305 if expandview: _mview(pdef,thisnode)
2306
2307 _mview( cxr_shader_params, _.layout )
2308
2309 def cxr_entity_changeclass(_,context):
2310 active_object = context.active_object
2311
2312 # Create ID properties
2313 entdef = None
2314 classname = cxr_custom_class(active_object)
2315
2316 if classname in cxr_entities:
2317 entdef = cxr_entities[classname]
2318
2319 kvs = entdef['keyvalues']
2320 if callable(kvs): kvs = kvs( {'object': active_object} )
2321
2322 for k in kvs:
2323 kv = kvs[k]
2324 key = F'cxrkv_{k}'
2325
2326 if callable(kv) or not isinstance(kv,dict): continue
2327
2328 if key not in active_object:
2329 active_object[key] = kv['default']
2330 id_prop = active_object.id_properties_ui(key)
2331 id_prop.update(default=kv['default'])
2332
2333 class CXR_ENTITY_PANEL(bpy.types.Panel):
2334 bl_label="Entity Config"
2335 bl_idname="SCENE_PT_convexer_entity"
2336 bl_space_type='PROPERTIES'
2337 bl_region_type='WINDOW'
2338 bl_context="object"
2339
2340 def draw(_,context):
2341 active_object = bpy.context.active_object
2342
2343 if active_object == None: return
2344
2345 default_context = {
2346 "scale": bpy.context.scene.cxr_data.scale_factor,
2347 "offset": (0,0,0)
2348 }
2349
2350 ecn = cxr_intrinsic_classname( active_object )
2351 classname = cxr_custom_class( active_object )
2352
2353 if ecn == None:
2354 if active_object.type == 'MESH':
2355 _.layout.prop( active_object.cxr_data, 'brushclass' )
2356 else: _.layout.prop( active_object.cxr_data, 'classname' )
2357
2358 _.layout.prop( active_object.cxr_data, 'visgroup' )
2359 _.layout.prop( active_object.cxr_data, 'lightmap_override' )
2360
2361 if classname == 'NONE':
2362 return
2363 else:
2364 _.layout.label(text=F"<implementation defined ({ecn})>")
2365 _.layout.enabled=False
2366 classname = ecn
2367
2368 kvs = cxr_entity_keyvalues( {
2369 "object": active_object,
2370 "transform": default_context,
2371 "classname": classname
2372 })
2373
2374 if kvs != None:
2375 for kv in kvs:
2376 if kv[1]:
2377 _.layout.prop( active_object, F'["cxrkv_{kv[0]}"]', text=kv[0])
2378 else:
2379 row = _.layout.row()
2380 row.enabled = False
2381 row.label( text=F'{kv[0]}: {repr(kv[2])}' )
2382 else:
2383 _.layout.label( text=F"ERROR: NO CLASS DEFINITION" )
2384
2385 class CXR_LIGHT_PANEL(bpy.types.Panel):
2386 bl_label = "Source Settings"
2387 bl_idname = "LIGHT_PT_cxr"
2388 bl_space_type = 'PROPERTIES'
2389 bl_region_type = 'WINDOW'
2390 bl_context = "data"
2391
2392 def draw(self, context):
2393 layout = self.layout
2394 scene = context.scene
2395
2396 active_object = bpy.context.active_object
2397 if active_object == None: return
2398
2399 if active_object.type == 'LIGHT' or \
2400 active_object.type == 'LIGHT_PROBE':
2401
2402 properties = active_object.data.cxr_data
2403
2404 if active_object.type == 'LIGHT':
2405 layout.prop( properties, "realtime" )
2406 elif active_object.type == 'LIGHT_PROBE':
2407 layout.prop( properties, "size" )
2408
2409 class CXR_COLLECTION_PANEL(bpy.types.Panel):
2410 bl_label = "Source Settings"
2411 bl_idname = "COL_PT_cxr"
2412 bl_space_type = 'PROPERTIES'
2413 bl_region_type = 'WINDOW'
2414 bl_context = "collection"
2415
2416 def draw(self, context):
2417 layout = self.layout
2418 scene = context.scene
2419
2420 active_collection = bpy.context.collection
2421
2422 if active_collection != None:
2423 layout.prop( active_collection.cxr_data, "shadow_caster" )
2424 layout.prop( active_collection.cxr_data, "texture_shadows" )
2425 layout.prop( active_collection.cxr_data, "preserve_order" )
2426 layout.prop( active_collection.cxr_data, "surfaceprop" )
2427 layout.prop( active_collection.cxr_data, "visgroup" )
2428
2429 # Settings groups
2430 # ------------------------------------------------------------------------------
2431
2432 class CXR_IMAGE_SETTINGS(bpy.types.PropertyGroup):
2433 export_res: bpy.props.IntVectorProperty(
2434 name="",
2435 description="Texture Export Resolution",
2436 default=(512,512),
2437 min=1,
2438 max=4096,
2439 size=2)
2440
2441 fmt: bpy.props.EnumProperty(
2442 name="Format",
2443 items = [
2444 ('DXT1', "DXT1", "BC1 compressed", '', 0),
2445 ('DXT5', "DXT5", "BC3 compressed", '', 1),
2446 ('RGB', "RGB", "Uncompressed", '', 2),
2447 ('RGBA', "RGBA", "Uncompressed (with alpha)", '', 3)
2448 ],
2449 description="Image format",
2450 default=0)
2451
2452 last_hash: bpy.props.StringProperty( name="" )
2453 asset_id: bpy.props.IntProperty(name="intl_assetid",default=0)
2454
2455 mipmap: bpy.props.BoolProperty(name="MIP",default=True)
2456 lod: bpy.props.BoolProperty(name="LOD",default=True)
2457 clamp: bpy.props.BoolProperty(name="CLAMP",default=False)
2458 flags: bpy.props.IntProperty(name="flags",default=0)
2459
2460 class CXR_LIGHT_SETTINGS(bpy.types.PropertyGroup):
2461 realtime: bpy.props.BoolProperty(name="Realtime Light", default=True)
2462
2463 class CXR_CUBEMAP_SETTINGS(bpy.types.PropertyGroup):
2464 size: bpy.props.EnumProperty(
2465 name="Resolution",
2466 items = [
2467 ('1',"1x1",'','',0),
2468 ('2',"2x2",'','',1),
2469 ('3',"4x4",'','',2),
2470 ('4',"8x8",'','',3),
2471 ('5',"16x16",'','',4),
2472 ('6',"32x32",'','',5),
2473 ('7',"64x64",'','',6),
2474 ('8',"128x128",'','',7),
2475 ('9',"256x256",'','',8)
2476 ],
2477 description="Texture resolution",
2478 default=7)
2479
2480 class CXR_ENTITY_SETTINGS(bpy.types.PropertyGroup):
2481 entity: bpy.props.BoolProperty(name="")
2482
2483 enum_pointents = [('NONE',"None","")]
2484 enum_brushents = [('NONE',"None","")]
2485
2486 for classname in cxr_entities:
2487 entdef = cxr_entities[classname]
2488 if 'allow' in entdef:
2489 itm = [(classname, classname, "")]
2490 if 'EMPTY' in entdef['allow']: enum_pointents += itm
2491 else: enum_brushents += itm
2492
2493 classname: bpy.props.EnumProperty(items=enum_pointents, name="Class", \
2494 update=cxr_entity_changeclass, default='NONE' )
2495
2496 brushclass: bpy.props.EnumProperty(items=enum_brushents, name="Class", \
2497 update=cxr_entity_changeclass, default='NONE' )
2498
2499 enum_classes = [('0',"None","")]
2500 for i, vg in enumerate(cxr_visgroups):
2501 enum_classes += [(str(i+1),vg,"")]
2502 visgroup: bpy.props.EnumProperty(name="visgroup",items=enum_classes,default=0)
2503 lightmap_override: bpy.props.IntProperty(name="Lightmap Override",default=0)
2504
2505 class CXR_MODEL_SETTINGS(bpy.types.PropertyGroup):
2506 last_hash: bpy.props.StringProperty( name="" )
2507 asset_id: bpy.props.IntProperty(name="vmf_settings",default=0)
2508 shadow_caster: bpy.props.BoolProperty( name="Shadow caster", default=True )
2509 texture_shadows: bpy.props.BoolProperty( name="Texture Shadows", default=False )
2510 preserve_order: bpy.props.BoolProperty( name="Preserve Order", default=False )
2511 surfaceprop: bpy.props.StringProperty( name="Suface prop",default="default" )
2512
2513 enum_classes = [('0',"None","")]
2514 for i, vg in enumerate(cxr_visgroups):
2515 enum_classes += [(str(i+1),vg,"")]
2516 visgroup: bpy.props.EnumProperty(name="visgroup",items=enum_classes,default=0)
2517
2518 class CXR_SCENE_SETTINGS(bpy.types.PropertyGroup):
2519 project_name: bpy.props.StringProperty( name="Project Name" )
2520 subdir: bpy.props.StringProperty( name="../csgo/ folder" )
2521
2522 exe_studiomdl: bpy.props.StringProperty( name="studiomdl" )
2523 exe_vbsp: bpy.props.StringProperty( name="vbsp" )
2524 opt_vbsp: bpy.props.StringProperty( name="args" )
2525 exe_vvis: bpy.props.StringProperty( name="vvis" )
2526 opt_vvis: bpy.props.StringProperty( name="args" )
2527 exe_vrad: bpy.props.StringProperty( name="vrad" )
2528 opt_vrad: bpy.props.StringProperty( name="args", \
2529 default="-reflectivityScale 0.35 -aoscale 1.4 -final -textureshadows -hdr -StaticPropLighting -StaticPropPolys" )
2530
2531 scale_factor: bpy.props.FloatProperty( name="VMF Scale factor", \
2532 default=32.0,min=1.0)
2533 skybox_scale_factor: bpy.props.FloatProperty( name="Sky Scale factor", \
2534 default=1.0,min=0.01)
2535 skyname: bpy.props.StringProperty(name="Skyname",default="sky_csgo_night02b")
2536 skybox_offset: bpy.props.FloatProperty(name="Sky offset",default=-4096.0)
2537 light_scale: bpy.props.FloatProperty(name="Light Scale",default=1.0/5.0)
2538 include_names: bpy.props.BoolProperty(name="Append original file names",\
2539 default=True)
2540 lightmap_scale: bpy.props.IntProperty(name="Global Lightmap Scale",\
2541 default=12)
2542 image_quality: bpy.props.IntProperty(name="Texture Quality (0-18)",\
2543 default=8, min=0, max=18 )
2544
2545 comp_vmf: bpy.props.BoolProperty(name="VMF",default=True)
2546 comp_models: bpy.props.BoolProperty(name="Models",default=True)
2547 comp_textures: bpy.props.BoolProperty(name="Textures",default=True)
2548 comp_compile: bpy.props.BoolProperty(name="Compile",default=True)
2549 comp_pack: bpy.props.BoolProperty(name="Pack",default=False)
2550
2551 classes = [ CXR_RELOAD, CXR_DEV_OPERATOR, CXR_INTERFACE, \
2552 CXR_MATERIAL_PANEL, CXR_IMAGE_SETTINGS,\
2553 CXR_MODEL_SETTINGS, CXR_ENTITY_SETTINGS, CXR_CUBEMAP_SETTINGS,\
2554 CXR_LIGHT_SETTINGS, CXR_SCENE_SETTINGS, CXR_DETECT_COMPILERS,\
2555 CXR_ENTITY_PANEL, CXR_LIGHT_PANEL, CXR_PREVIEW_OPERATOR,\
2556 CXR_VIEW3D, CXR_COMPILER_CHAIN, CXR_RESET_HASHES,\
2557 CXR_COMPILE_MATERIAL, CXR_COLLECTION_PANEL, CXR_RESET ]
2558
2559 vmt_param_dynamic_class = None
2560
2561 def register():
2562 global cxr_view_draw_handler, vmt_param_dynamic_class, cxr_ui_handler
2563
2564 for c in classes:
2565 bpy.utils.register_class(c)
2566
2567 # Build dynamic VMT properties class defined by cxr_shader_params
2568 annotations_dict = {}
2569
2570 def _dvmt_propogate(layer):
2571 nonlocal annotations_dict
2572
2573 for decl in layer:
2574 if isinstance(layer[decl],dict): # $property definition
2575 pdef = layer[decl]
2576
2577 prop = None
2578 if pdef['type'] == 'bool':
2579 prop = bpy.props.BoolProperty(\
2580 name = pdef['name'],\
2581 default = pdef['default'])
2582
2583 elif pdef['type'] == 'float':
2584 prop = bpy.props.FloatProperty(\
2585 name = pdef['name'],\
2586 default = pdef['default'])
2587
2588 elif pdef['type'] == 'vector':
2589 if 'subtype' in pdef:
2590 prop = bpy.props.FloatVectorProperty(\
2591 name = pdef['name'],\
2592 subtype = pdef['subtype'],\
2593 default = pdef['default'],\
2594 size = len(pdef['default']))
2595 else:
2596 prop = bpy.props.FloatVectorProperty(\
2597 name = pdef['name'],\
2598 default = pdef['default'],\
2599 size = len(pdef['default']))
2600
2601 elif pdef['type'] == 'string':
2602 prop = bpy.props.StringProperty(\
2603 name = pdef['name'],\
2604 default = pdef['default'])
2605
2606 elif pdef['type'] == 'enum':
2607 prop = bpy.props.EnumProperty(\
2608 name = pdef['name'],\
2609 items = pdef['items'],\
2610 default = pdef['default'])
2611
2612 if prop != None:
2613 annotations_dict[decl] = prop
2614
2615 # Recurse into sub-definitions
2616 _dvmt_propogate(pdef)
2617
2618 annotations_dict["shader"] = bpy.props.EnumProperty(\
2619 name = "Shader",\
2620 items = [( _,\
2621 cxr_shaders[_]["name"],\
2622 '') for _ in cxr_shaders],\
2623 default = next(iter(cxr_shaders)))
2624
2625 annotations_dict["asset_id"] = bpy.props.IntProperty(name="intl_assetid",\
2626 default=0)
2627
2628 _dvmt_propogate( cxr_shader_params )
2629 vmt_param_dynamic_class = type(
2630 "CXR_VMT_DYNAMIC",
2631 (bpy.types.PropertyGroup,),{
2632 "__annotations__": annotations_dict
2633 },
2634 )
2635
2636 bpy.utils.register_class( vmt_param_dynamic_class )
2637
2638 # Pointer types
2639 bpy.types.Material.cxr_data = \
2640 bpy.props.PointerProperty(type=vmt_param_dynamic_class)
2641 bpy.types.Image.cxr_data = \
2642 bpy.props.PointerProperty(type=CXR_IMAGE_SETTINGS)
2643 bpy.types.Object.cxr_data = \
2644 bpy.props.PointerProperty(type=CXR_ENTITY_SETTINGS)
2645 bpy.types.Collection.cxr_data = \
2646 bpy.props.PointerProperty(type=CXR_MODEL_SETTINGS)
2647 bpy.types.Light.cxr_data = \
2648 bpy.props.PointerProperty(type=CXR_LIGHT_SETTINGS)
2649 bpy.types.LightProbe.cxr_data = \
2650 bpy.props.PointerProperty(type=CXR_CUBEMAP_SETTINGS)
2651 bpy.types.Scene.cxr_data = \
2652 bpy.props.PointerProperty(type=CXR_SCENE_SETTINGS)
2653
2654 # CXR Scene settings
2655
2656 # GPU / callbacks
2657 cxr_view_draw_handler = bpy.types.SpaceView3D.draw_handler_add(\
2658 cxr_draw,(),'WINDOW','POST_VIEW')
2659
2660 cxr_ui_handler = bpy.types.SpaceView3D.draw_handler_add(\
2661 cxr_ui,(None,None),'WINDOW','POST_PIXEL')
2662
2663 bpy.app.handlers.load_post.append(cxr_on_load)
2664 bpy.app.handlers.depsgraph_update_post.append(cxr_dgraph_update)
2665
2666 def unregister():
2667 global cxr_view_draw_handler, vmt_param_dynamic_class, cxr_ui_handler
2668
2669 bpy.utils.unregister_class( vmt_param_dynamic_class )
2670 for c in classes:
2671 bpy.utils.unregister_class(c)
2672
2673 bpy.app.handlers.depsgraph_update_post.remove(cxr_dgraph_update)
2674 bpy.app.handlers.load_post.remove(cxr_on_load)
2675
2676 bpy.types.SpaceView3D.draw_handler_remove(cxr_view_draw_handler,'WINDOW')
2677 bpy.types.SpaceView3D.draw_handler_remove(cxr_ui_handler,'WINDOW')