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