UI and interface
[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
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 if CXR_PREVIEW_OPERATOR.LASTERR != None:
86 blf.position(0,2,80,0)
87 blf.size(0,50,48)
88 blf.color(0,1.0,0.2,0.2,0.9)
89 blf.draw(0,"This is a stoopid error\nWIthiuawdnaw")
90
91 # Something is off with TIMER,
92 # this forces the viewport to redraw before we can continue with our
93 # compilation stuff.
94
95 CXR_COMPILER_CHAIN.WAIT_REDRAW = False
96
97 def cxr_draw():
98 global cxr_view_shader, cxr_view_mesh, cxr_view_lines
99
100 cxr_view_shader.bind()
101
102 gpu.state.depth_mask_set(False)
103 gpu.state.line_width_set(1.5)
104 gpu.state.face_culling_set('BACK')
105 gpu.state.depth_test_set('NONE')
106 gpu.state.blend_set('ALPHA')
107
108 if cxr_view_lines != None:
109 cxr_view_lines.draw( cxr_view_shader )
110
111 gpu.state.depth_test_set('LESS_EQUAL')
112 gpu.state.blend_set('ADDITIVE')
113 if cxr_view_mesh != None:
114 cxr_view_mesh.draw( cxr_view_shader )
115
116 def cxr_jobs_update_graph(jobs):
117 global cxr_jobs_batch, cxr_ui_shader, cxr_jobs_inf
118
119 cxr_jobs_inf = []
120
121 total_width = 0
122 verts = []
123 colours = []
124 indices = []
125
126 for sys in jobs:
127 total_width += sys['w']
128
129 sf = 1.0/total_width
130 cur = 0.0
131 ci = 0
132
133 for sys in jobs:
134 w = sys['w']
135 h = 30.0
136 colour = sys['colour']
137 colourwait = (colour[0],colour[1],colour[2],0.4)
138 colourrun = (colour[0]*1.5,colour[1]*1.5,colour[2]*1.5,0.5)
139 colourdone = (colour[0],colour[1],colour[2],1.0)
140
141 jobs = sys['jobs']
142 sfsub = (1.0/(len(jobs)))*w
143 i = 0
144
145 for j in jobs:
146 if j == None: colour = colourdone
147 else: colour = colourwait
148
149 px = (cur + (i)*sfsub) * sf
150 px1 = (cur + (i+1.0)*sfsub) * sf - 0.003
151 i += 1
152
153 verts += [(px,0), (px, h), (px1, 0.0), (px1,h)]
154 colours += [colour,colour,colour,colour]
155 indices += [(ci+0,ci+2,ci+3),(ci+0,ci+3,ci+1)]
156 ci += 4
157
158 cxr_jobs_inf += [((sf*cur), sys['title'])]
159 cur += w
160
161 cxr_jobs_batch = batch_for_shader(
162 cxr_ui_shader, 'TRIS',
163 { "aPos": verts, "aColour": colours },
164 indices = indices
165 )
166
167 # view_layer.update() doesnt seem to work,
168 # tag_redraw() seems to have broken
169 # therefore, change a property
170 def scene_redraw():
171 ob = bpy.context.scene.objects[0]
172 ob.hide_render = ob.hide_render
173
174 # the 'real' way to refresh the scene
175 for area in bpy.context.window.screen.areas:
176 if area.type == 'view_3d':
177 area.tag_redraw()
178
179 # Shared libraries
180 # ------------------------------------------------------------------------------
181
182 # dlclose for reloading modules manually
183 libc_dlclose = None
184 libc_dlclose = cdll.LoadLibrary(None).dlclose
185 libc_dlclose.argtypes = [c_void_p]
186
187 # wrapper for ctypes binding
188 class extern():
189 def __init__(_,name,argtypes,restype):
190 _.name = name
191 _.argtypes = argtypes
192 _.restype = restype
193 _.call = None
194
195 def loadfrom(_,so):
196 _.call = getattr(so,_.name)
197 _.call.argtypes = _.argtypes
198
199 if _.restype != None:
200 _.call.restype = _.restype
201
202 # libcxr (convexer)
203 # ------------------------------------------------------------------------------
204
205 libcxr = None
206
207 # Structure definitions
208 #
209 class cxr_edge(Structure):
210 _fields_ = [("i0",c_int32),
211 ("i1",c_int32),
212 ("freestyle",c_int32)]
213
214 class cxr_static_loop(Structure):
215 _fields_ = [("index",c_int32),
216 ("edge_index",c_int32),
217 ("uv",c_double * 2)]
218
219 class cxr_polygon(Structure):
220 _fields_ = [("loop_start",c_int32),
221 ("loop_total",c_int32),
222 ("normal",c_double * 3),
223 ("center",c_double * 3),
224 ("material_id",c_int32)]
225
226 class cxr_material(Structure):
227 _fields_ = [("res",c_int32 * 2),
228 ("name",c_char_p)]
229
230 class cxr_static_mesh(Structure):
231 _fields_ = [("vertices",POINTER(c_double * 3)),
232 ("edges",POINTER(cxr_edge)),
233 ("loops",POINTER(cxr_static_loop)),
234 ("polys",POINTER(cxr_polygon)),
235 ("materials",POINTER(cxr_material)),
236
237 ("poly_count",c_int32),
238 ("vertex_count",c_int32),
239 ("edge_count",c_int32),
240 ("loop_count",c_int32),
241 ("material_count",c_int32)]
242
243 class cxr_tri_mesh(Structure):
244 _fields_ = [("vertices",POINTER(c_double *3)),
245 ("colours",POINTER(c_double *4)),
246 ("indices",POINTER(c_int32)),
247 ("indices_count",c_int32),
248 ("vertex_count",c_int32)]
249
250 class cxr_vmf_context(Structure):
251 _fields_ = [("mapversion",c_int32),
252 ("skyname",c_char_p),
253 ("detailvbsp",c_char_p),
254 ("detailmaterial",c_char_p),
255 ("scale",c_double),
256 ("offset",c_double *3),
257 ("lightmap_scale",c_int32),
258 ("brush_count",c_int32),
259 ("entity_count",c_int32),
260 ("face_count",c_int32)]
261
262 # Convert blenders mesh format into CXR's static format (they are very similar)
263 #
264 def mesh_cxr_format(obj):
265 orig_state = None
266
267 if bpy.context.active_object != None:
268 orig_state = obj.mode
269 if orig_state != 'OBJECT':
270 bpy.ops.object.mode_set(mode='OBJECT')
271
272 dgraph = bpy.context.evaluated_depsgraph_get()
273 data = obj.evaluated_get(dgraph).data
274
275 _,mtx_rot,_ = obj.matrix_world.decompose()
276
277 mesh = cxr_static_mesh()
278
279 vertex_data = ((c_double*3)*len(data.vertices))()
280 for i, vert in enumerate(data.vertices):
281 v = obj.matrix_world @ vert.co
282 vertex_data[i][0] = c_double(v[0])
283 vertex_data[i][1] = c_double(v[1])
284 vertex_data[i][2] = c_double(v[2])
285
286 loop_data = (cxr_static_loop*len(data.loops))()
287 polygon_data = (cxr_polygon*len(data.polygons))()
288
289 for i, poly in enumerate(data.polygons):
290 loop_start = poly.loop_start
291 loop_end = poly.loop_start + poly.loop_total
292 for loop_index in range(loop_start, loop_end):
293 loop = data.loops[loop_index]
294 loop_data[loop_index].index = loop.vertex_index
295 loop_data[loop_index].edge_index = loop.edge_index
296
297 if data.uv_layers:
298 uv = data.uv_layers.active.data[loop_index].uv
299 loop_data[loop_index].uv[0] = c_double(uv[0])
300 loop_data[loop_index].uv[1] = c_double(uv[1])
301 else:
302 loop_data[loop_index].uv[0] = c_double(0.0)
303 loop_data[loop_index].uv[1] = c_double(0.0)
304 center = obj.matrix_world @ poly.center
305 normal = mtx_rot @ poly.normal
306
307 polygon_data[i].loop_start = poly.loop_start
308 polygon_data[i].loop_total = poly.loop_total
309 polygon_data[i].normal[0] = normal[0]
310 polygon_data[i].normal[1] = normal[1]
311 polygon_data[i].normal[2] = normal[2]
312 polygon_data[i].center[0] = center[0]
313 polygon_data[i].center[1] = center[1]
314 polygon_data[i].center[2] = center[2]
315 polygon_data[i].material_id = poly.material_index
316
317 edge_data = (cxr_edge*len(data.edges))()
318
319 for i, edge in enumerate(data.edges):
320 edge_data[i].i0 = edge.vertices[0]
321 edge_data[i].i1 = edge.vertices[1]
322 edge_data[i].freestyle = edge.use_freestyle_mark
323
324 material_data = (cxr_material*len(obj.material_slots))()
325
326 for i, ms in enumerate(obj.material_slots):
327 inf = material_info(ms.material)
328 material_data[i].res[0] = inf['res'][0]
329 material_data[i].res[1] = inf['res'][1]
330 material_data[i].name = inf['name'].encode('utf-8')
331
332 mesh.edges = cast(edge_data, POINTER(cxr_edge))
333 mesh.vertices = cast(vertex_data, POINTER(c_double*3))
334 mesh.loops = cast(loop_data,POINTER(cxr_static_loop))
335 mesh.polys = cast(polygon_data, POINTER(cxr_polygon))
336 mesh.materials = cast(material_data, POINTER(cxr_material))
337
338 mesh.poly_count = len(data.polygons)
339 mesh.vertex_count = len(data.vertices)
340 mesh.edge_count = len(data.edges)
341 mesh.loop_count = len(data.loops)
342 mesh.material_count = len(obj.material_slots)
343
344 if orig_state != None:
345 bpy.ops.object.mode_set(mode=orig_state)
346
347 return mesh
348
349 # Callback ctypes indirection things.. not really sure.
350 c_libcxr_log_callback = None
351 c_libcxr_line_callback = None
352
353 # Public API
354 # -------------------------------------------------------------
355 libcxr_decompose = extern( "cxr_decompose",
356 [POINTER(cxr_static_mesh), POINTER(c_int32)],
357 c_void_p
358 )
359 libcxr_free_world = extern( "cxr_free_world",
360 [c_void_p],
361 None
362 )
363 libcxr_write_test_data = extern( "cxr_write_test_data",
364 [POINTER(cxr_static_mesh)],
365 None
366 )
367 libcxr_world_preview = extern( "cxr_world_preview",
368 [c_void_p],
369 POINTER(cxr_tri_mesh)
370 )
371 libcxr_free_tri_mesh = extern( "cxr_free_tri_mesh",
372 [c_void_p],
373 None
374 )
375 libcxr_begin_vmf = extern( "cxr_begin_vmf",
376 [POINTER(cxr_vmf_context), c_void_p],
377 None
378 )
379 libcxr_vmf_begin_entities = extern( "cxr_vmf_begin_entities",
380 [POINTER(cxr_vmf_context), c_void_p],
381 None
382 )
383 libcxr_push_world_vmf = extern("cxr_push_world_vmf",
384 [c_void_p,POINTER(cxr_vmf_context),c_void_p],
385 None
386 )
387 libcxr_end_vmf = extern( "cxr_end_vmf",
388 [POINTER(cxr_vmf_context),c_void_p],
389 None
390 )
391
392 # VDF + with open wrapper
393 libcxr_vdf_open = extern( "cxr_vdf_open", [c_char_p], c_void_p )
394 libcxr_vdf_close = extern( "cxr_vdf_close", [c_void_p], None )
395 libcxr_vdf_put = extern( "cxr_vdf_put", [c_void_p,c_char_p], None )
396 libcxr_vdf_node = extern( "cxr_vdf_node", [c_void_p,c_char_p], None )
397 libcxr_vdf_edon = extern( "cxr_vdf_edon", [c_void_p], None )
398 libcxr_vdf_kv = extern( "cxr_vdf_kv", [c_void_p,c_char_p,c_char_p], None )
399
400 class vdf_structure():
401 def __init__(_,path):
402 _.path = path
403 def __enter__(_):
404 _.fp = libcxr_vdf_open.call( _.path.encode('utf-8') )
405 if _.fp == None:
406 print( F"Could not open file {_.path}" )
407 return None
408 return _
409 def __exit__(_,type,value,traceback):
410 if _.fp != None:
411 libcxr_vdf_close.call(_.fp)
412 def put(_,s):
413 libcxr_vdf_put.call(_.fp, s.encode('utf-8') )
414 def node(_,name):
415 libcxr_vdf_node.call(_.fp, name.encode('utf-8') )
416 def edon(_):
417 libcxr_vdf_edon.call(_.fp)
418 def kv(_,k,v):
419 libcxr_vdf_kv.call(_.fp, k.encode('utf-8'), v.encode('utf-8'))
420
421 # Other
422 libcxr_lightpatch_bsp = extern( "cxr_lightpatch_bsp", [c_char_p], None )
423
424 libcxr_funcs = [ libcxr_decompose, libcxr_free_world, libcxr_begin_vmf, \
425 libcxr_vmf_begin_entities, libcxr_push_world_vmf, \
426 libcxr_end_vmf, libcxr_vdf_open, libcxr_vdf_close, \
427 libcxr_vdf_put, libcxr_vdf_node, libcxr_vdf_edon,
428 libcxr_vdf_kv, libcxr_lightpatch_bsp, libcxr_write_test_data,\
429 libcxr_world_preview, libcxr_free_tri_mesh ]
430
431 # Callbacks
432
433 def libcxr_log_callback(logStr):
434 print( F"{logStr.decode('utf-8')}",end='' )
435
436 cxr_line_positions = None
437 cxr_line_colours = None
438
439 def cxr_reset_lines():
440 global cxr_line_positions, cxr_line_colours
441
442 cxr_line_positions = []
443 cxr_line_colours = []
444
445 def cxr_batch_lines():
446 global cxr_line_positions, cxr_line_colours, cxr_view_shader, cxr_view_lines
447
448 cxr_view_lines = batch_for_shader(\
449 cxr_view_shader, 'LINES',\
450 { "pos": cxr_line_positions, "color": cxr_line_colours })
451
452 def libcxr_line_callback( p0,p1,colour ):
453 global cxr_line_colours, cxr_line_positions
454
455 cxr_line_positions += [(p0[0],p0[1],p0[2])]
456 cxr_line_positions += [(p1[0],p1[1],p1[2])]
457 cxr_line_colours += [(colour[0],colour[1],colour[2],colour[3])]
458 cxr_line_colours += [(colour[0],colour[1],colour[2],colour[3])]
459
460 # libnbvtf
461 # ------------------------------------------------------------------------------
462
463 libnbvtf = None
464
465 # Constants
466 NBVTF_IMAGE_FORMAT_RGBA8888 = 0
467 NBVTF_IMAGE_FORMAT_RGB888 = 2
468 NBVTF_IMAGE_FORMAT_DXT1 = 13
469 NBVTF_IMAGE_FORMAT_DXT5 = 15
470 NBVTF_TEXTUREFLAGS_CLAMPS = 0x00000004
471 NBVTF_TEXTUREFLAGS_CLAMPT = 0x00000008
472 NBVTF_TEXTUREFLAGS_NORMAL = 0x00000080
473 NBVTF_TEXTUREFLAGS_NOMIP = 0x00000100
474 NBVTF_TEXTUREFLAGS_NOLOD = 0x00000200
475
476 libnbvtf_convert = extern( "nbvtf_convert", \
477 [c_char_p,c_int32,c_int32,c_int32,c_int32,c_int32,c_uint32,c_char_p], \
478 c_int32 )
479
480 libnbvtf_init = extern( "nbvtf_init", [], None )
481 libnbvtf_funcs = [ libnbvtf_convert, libnbvtf_init ]
482
483 # Loading
484 # --------------------------
485
486 def shared_reload():
487 global libcxr, libnbvtf, libcxr_funcs, libnbvtf_funcs
488
489 # Unload libraries if existing
490 def _reload( lib, path ):
491 if lib != None:
492 _handle = lib._handle
493 for i in range(10): libc_dlclose( _handle )
494 lib = None
495 del lib
496 return cdll.LoadLibrary( F'{os.path.dirname(__file__)}/{path}.so' )
497
498 libnbvtf = _reload( libnbvtf, "libnbvtf" )
499 libcxr = _reload( libcxr, "libcxr" )
500
501 for fd in libnbvtf_funcs:
502 fd.loadfrom( libnbvtf )
503 libnbvtf_init.call()
504
505 for fd in libcxr_funcs:
506 fd.loadfrom( libcxr )
507
508 # Callbacks
509 global c_libcxr_log_callback, c_libcxr_line_callback
510
511 LOG_FUNCTION_TYPE = CFUNCTYPE(None,c_char_p)
512 c_libcxr_log_callback = LOG_FUNCTION_TYPE(libcxr_log_callback)
513
514 LINE_FUNCTION_TYPE = CFUNCTYPE(None,\
515 POINTER(c_double), POINTER(c_double), POINTER(c_double))
516 c_libcxr_line_callback = LINE_FUNCTION_TYPE(libcxr_line_callback)
517
518 libcxr.cxr_set_log_function(cast(c_libcxr_log_callback,c_void_p))
519 libcxr.cxr_set_line_function(cast(c_libcxr_line_callback,c_void_p))
520
521 build_time = c_char_p.in_dll(libcxr,'cxr_build_time')
522 print( F"libcxr build time: {build_time.value}" )
523
524 shared_reload()
525
526 # Configuration
527 # ------------------------------------------------------------------------------
528
529 # Standard entity functions, think of like base.fgd
530 #
531 def cxr_get_origin(obj,context):
532 return obj.location * context['scale'] + mathutils.Vector(context['offset'])
533
534 def cxr_get_angles(obj,context):
535 euler = [ a*57.295779513 for a in obj.rotation_euler ]
536 angle = [0,0,0]
537 angle[0] = euler[1]
538 angle[1] = euler[2]
539 angle[2] = euler[0]
540 return angle
541
542 def cxr_baseclass(classes, other):
543 base = other.copy()
544 for x in classes:
545 base.update(x.copy())
546 return base
547
548 # EEVEE Light component converter -> Source 1
549 #
550 def ent_lights(obj,context):
551 kvs = cxr_baseclass([ent_origin],\
552 {
553 "_distance": (0.0 if obj.data.cxr_data.realtime else -1.0),
554 "_light": [int(pow(obj.data.color[i],1.0/2.2)*255.0) for i in range(3)] +\
555 [int(obj.data.energy * bpy.context.scene.cxr_data.light_scale)],
556 "_lightHDR": '-1 -1 -1 1',
557 "_lightscaleHDR": 1
558 })
559
560 if obj.data.type == 'SPOT':
561 kvs['_cone'] = obj.data.spot_size*(57.295779513/2.0)
562 kvs['_inner_cone'] = (1.0-obj.data.spot_blend)*kvs['_cone']
563
564 # Blenders spotlights are -z forward
565 # Source is +x, however, it seems to use a completely different system.
566 # Since we dont care about roll for spotlights, we just take the
567 # pitch and yaw via trig
568
569 _,mtx_rot,_ = obj.matrix_world.decompose()
570 fwd = mtx_rot @ mathutils.Vector((0,0,-1))
571
572 kvs['pitch'] = math.asin(fwd[2]) * 57.295779513
573 kvs['angles'] = [ 0.0, math.atan2(fwd[1],fwd[0]) * 57.295779513, 0.0 ]
574 kvs['_quadratic_attn'] = 0.0 # Source spotlights + quadratic falloff look
575 # Really bad...
576 #
577 # Blender's default has a much more 'nice'
578 # look.
579 kvs['_linear_attn'] = 1.0
580
581 elif obj.data.type == 'POINT':
582 kvs['_quadratic_attn'] = 1.0
583 kvs['_linear_attn'] = 0.0
584
585 elif obj.data.type == 'SUN':
586 pass # TODO
587
588 return kvs
589
590 def ent_cubemap(obj,context):
591 return cxr_baseclass([ent_origin], {"cubemapsize": obj.data.cxr_data.size})
592
593 ent_origin = { "origin": cxr_get_origin }
594 ent_angles = { "angles": cxr_get_angles }
595 ent_transform = cxr_baseclass( [ent_origin], ent_angles )
596
597 #include the user config
598 exec(open(F'{os.path.dirname(__file__)}/config.py').read())
599
600 # Blender state callbacks
601 # ------------------------------------------------------------------------------
602
603 @persistent
604 def cxr_on_load(dummy):
605 global cxr_view_lines, cxr_view_mesh
606
607 cxr_view_lines = None
608 cxr_view_mesh = None
609
610 @persistent
611 def cxr_dgraph_update(scene,dgraph):
612 return
613 print( F"Hallo {time.time()}" )
614
615 # Convexer compilation functions
616 # ------------------------------------------------------------------------------
617
618 # Asset path management
619
620 def asset_uid(asset):
621 if isinstance(asset,str):
622 return asset
623
624 # Create a unique ID string
625 base = "ABCDEFGHIJKLMNOPQRSTUV"
626 v = asset.cxr_data.asset_id
627 name = ""
628
629 if v == 0:
630 name = "A"
631 else:
632 dig = []
633
634 while v:
635 dig.append( int( v % len(base) ) )
636 v //= len(base)
637
638 for d in dig[::-1]:
639 name += base[d]
640
641 if bpy.context.scene.cxr_data.include_names:
642 name += asset.name.replace('.','_')
643
644 return name
645
646 # -> <project_name>/<asset_name>
647 def asset_name(asset):
648 return F"{bpy.context.scene.cxr_data.project_name}/{asset_uid(asset)}"
649
650 # -> <subdir>/<project_name>/<asset_name>
651 def asset_path(subdir, asset):
652 return F"{subdir}/{asset_name(asset_uid(asset))}"
653
654 # -> <csgo>/<subdir>/<project_name>/<asset_name>
655 def asset_full_path(sdir,asset):
656 return F"{bpy.context.scene.cxr_data.subdir}/"+\
657 F"{asset_path(sdir,asset_uid(asset))}"
658
659 # Entity functions / infos
660 # ------------------------
661
662 def cxr_intrinsic_classname(obj):
663 if obj.type == 'LIGHT':
664 return {
665 'SPOT': "light_spot",
666 'POINT': "light",
667 'SUN': "light_directional" }[ obj.data.type ]
668
669 elif obj.type == 'LIGHT_PROBE':
670 return "env_cubemap"
671 elif obj.type == 'EMPTY':
672 if obj.is_instancer:
673 return "prop_static"
674
675 return None
676
677 def cxr_custom_class(obj):
678 if obj.type == 'MESH': custom_class = obj.cxr_data.brushclass
679 else: custom_class = obj.cxr_data.classname
680
681 return custom_class
682
683 def cxr_classname(obj):
684 intr = cxr_intrinsic_classname(obj)
685 if intr != None: return intr
686
687 custom_class = cxr_custom_class(obj)
688 if custom_class != 'NONE':
689 return custom_class
690
691 return None
692
693 # Returns array of:
694 # intinsic: (k, False, value)
695 # property: (k, True, value or default)
696 #
697 # Error: None
698 #
699 def cxr_entity_keyvalues(obj,context,classname):
700 if classname not in cxr_entities: return None
701
702 result = []
703
704 entdef = cxr_entities[classname]
705 kvs = entdef['keyvalues']
706
707 if callable(kvs): kvs = kvs(obj, context)
708
709 for k in kvs:
710 kv = kvs[k]
711 value = kv
712 isprop = False
713
714 if isinstance(kv,dict):
715 isprop = True
716 value = obj[ F"cxrkv_{k}" ]
717 else:
718 if callable(kv):
719 value = kv(obj,context)
720
721 if isinstance(value,mathutils.Vector):
722 value = [_ for _ in value]
723
724 result += [(k, isprop, value)]
725
726 return result
727
728 # Extract material information from shader graph data
729 #
730 def material_info(mat):
731 info = {}
732 info['res'] = (512,512)
733 info['name'] = 'tools/toolsnodraw'
734
735 if mat == None or mat.use_nodes == False:
736 return info
737
738 # Builtin shader
739 if mat.cxr_data.shader == 'Builtin':
740 info['name'] = mat.name
741 return info
742
743 # Custom materials
744 info['name'] = asset_name(mat)
745
746 # Using the cxr_graph_mapping as a reference, go through the shader
747 # graph and gather all $props from it.
748 #
749 def _graph_read( node_def, node=None, depth=0 ):
750 nonlocal mat
751 nonlocal info
752
753 def _variant_apply( val ):
754 nonlocal mat
755
756 if isinstance( val, str ):
757 return val
758 else:
759 for shader_variant in val:
760 if shader_variant[0] == mat.cxr_data.shader:
761 return shader_variant[1]
762
763 # Find rootnodes
764 if node == None:
765 _graph_read.extracted = []
766
767 for node_idname in node_def:
768 for n in mat.node_tree.nodes:
769 if n.bl_idname == node_idname:
770 node_def = node_def[node_idname]
771 node = n
772 break
773
774 for link in node_def:
775 if isinstance( node_def[link], dict ):
776 inputt = node.inputs[link]
777 inputt_def = node_def[link]
778
779 if inputt.is_linked:
780
781 # look for definitions for the connected node type
782 con = inputt.links[0].from_node
783
784 for node_idname in inputt_def:
785 if con.bl_idname == node_idname:
786 con_def = inputt_def[ node_idname ]
787 _graph_read( con_def, con, depth+1 )
788
789 # No definition found! :(
790 # TODO: Make a warning for this?
791
792 else:
793 if "default" in inputt_def:
794 prop = _variant_apply( inputt_def['default'] )
795 info[prop] = inputt.default_value
796 else:
797 prop = _variant_apply( node_def[link] )
798 info[prop] = getattr(node,link)
799
800 _graph_read(cxr_graph_mapping)
801
802 if "$basetexture" in info:
803 export_res = info['$basetexture'].cxr_data.export_res
804 info['res'] = (export_res[0], export_res[1])
805
806 return info
807
808 # Prepares Scene into dictionary format
809 #
810 def cxr_scene_collect():
811 context = bpy.context
812
813 # Make sure all of our asset types have a unique ID
814 def _uid_prepare(objtype):
815 used_ids = [0]
816 to_generate = []
817 id_max = 0
818 for o in objtype:
819 vs = o.cxr_data
820 if vs.asset_id in used_ids:
821 to_generate+=[vs]
822 else:
823 id_max = max(id_max,vs.asset_id)
824 used_ids+=[vs.asset_id]
825 for vs in to_generate:
826 id_max += 1
827 vs.asset_id = id_max
828 _uid_prepare(bpy.data.materials)
829 _uid_prepare(bpy.data.images)
830 _uid_prepare(bpy.data.collections)
831
832 sceneinfo = {
833 "entities": [], # Everything with a classname
834 "geo": [], # All meshes without a classname
835 "heros": [] # Collections prefixed with mdl_
836 }
837
838 def _collect(collection,transform):
839 nonlocal sceneinfo
840
841 if collection.name.startswith('.'): return
842 if collection.hide_render: return
843
844 if collection.name.startswith('mdl_'):
845 sceneinfo['heros'] += [{
846 "collection": collection,
847 "transform": transform
848 }]
849 return
850
851 for obj in collection.objects:
852 if obj.hide_get(): continue
853
854 classname = cxr_classname( obj )
855
856 if classname != None:
857 sceneinfo['entities'] += [{
858 "object": obj,
859 "classname": classname,
860 "transform": transform
861 }]
862 elif obj.type == 'MESH':
863 sceneinfo['geo'] += [{
864 "object": obj,
865 "transform": transform
866 }]
867
868 for c in collection.children:
869 cxr_scene_collect( c, transform )
870
871 transform_main = {
872 "scale": context.scene.cxr_data.scale_factor,
873 "offset": (0,0,0)
874 }
875
876 transform_sky = {
877 "scale": context.scene.cxr_data.skybox_scale_factor,
878 "offset": (0,0,context.scene.cxr_data.skybox_offset )
879 }
880
881 if 'main' in bpy.data.collections:
882 _collect( bpy.data.collections['main'], transform_main )
883
884 if 'skybox' in bpy.data.collections:
885 _collect( bpy.data.collections['skybox'], transform_sky )
886
887 return sceneinfo
888
889 # Write VMF out to file (JOB HANDLER)
890 #
891 def cxr_export_vmf(sceneinfo):
892 cxr_reset_lines()
893
894 # Setup output and state
895 filepath = bpy.data.filepath
896 directory = os.path.dirname(filepath)
897 settings = bpy.context.scene.cxr_data
898
899 asset_dir = F"{directory}/bin"
900 material_dir = F"{settings.subdir}/materials/{settings.project_name}"
901 model_dir = F"{settings.subdir}/models/{settings.project_name}"
902
903 os.makedirs( asset_dir, exist_ok=True )
904 os.makedirs( material_dir, exist_ok=True )
905 os.makedirs( model_dir, exist_ok=True )
906
907 # States
908 cxr_reset_lines()
909 output_vmf = F"{directory}/{settings.project_name}.vmf"
910
911 with vdf_structure(output_vmf) as m:
912 print( F"Write: {output_vmf}" )
913
914 vmfinfo = cxr_vmf_context()
915 vmfinfo.mapversion = 4
916
917 #TODO: These need to be in options...
918 vmfinfo.skyname = b"sky_csgo_night02b"
919 vmfinfo.detailvbsp = b"detail.vbsp"
920 vmfinfo.detailmaterial = b"detail/detailsprites"
921 vmfinfo.lightmap_scale = 12
922
923 vmfinfo.brush_count = 0
924 vmfinfo.entity_count = 0
925 vmfinfo.face_count = 0
926
927 libcxr_begin_vmf.call( pointer(vmfinfo), m.fp )
928
929 def _buildsolid( cmd ):
930 nonlocal m
931
932 baked = mesh_cxr_format( cmd['object'] )
933 world = libcxr_decompose.call( baked, None )
934
935 if world == None:
936 return False
937
938 vmfinfo.scale = cmd['transform']['scale']
939
940 offset = cmd['transform']['offset']
941 vmfinfo.offset[0] = offset[0]
942 vmfinfo.offset[1] = offset[1]
943 vmfinfo.offset[2] = offset[2]
944
945 libcxr_push_world_vmf.call( world, pointer(vmfinfo), m.fp )
946 libcxr_free_world.call( world )
947
948 return True
949
950 # World geometry
951 for brush in sceneinfo['geo']:
952 if not _buildsolid( brush ):
953 cxr_batch_lines()
954 scene_redraw()
955 return False
956
957 libcxr_vmf_begin_entities.call(pointer(vmfinfo), m.fp)
958
959 # Entities
960 for ent in sceneinfo['entities']:
961 obj = ent['object']
962 ctx = ent['transform']
963 cls = ent['classname']
964
965 m.node( 'entity' )
966 m.kv( 'classname', cls )
967
968 kvs = cxr_entity_keyvalues( obj, ctx, cls )
969
970 for kv in kvs:
971 if isinstance(kv[2], list):
972 m.kv( kv[0], ' '.join([str(_) for _ in kv[2]]) )
973 else: m.kv( kv[0], str(kv[2]) )
974
975 if obj.type == 'MESH':
976 if not _buildsolid( ent ):
977 cxr_batch_lines()
978 scene_redraw()
979 return False
980
981 m.edon()
982
983 print( "Done" )
984 return True
985
986 # COmpile image using NBVTF and hash it (JOB HANDLER)
987 #
988 def compile_image(img):
989 if img==None:
990 return None
991
992 name = asset_name(img)
993 src_path = bpy.path.abspath(img.filepath)
994
995 dims = img.cxr_data.export_res
996 fmt = {
997 'RGBA': NBVTF_IMAGE_FORMAT_RGBA8888,
998 'DXT1': NBVTF_IMAGE_FORMAT_DXT1,
999 'DXT5': NBVTF_IMAGE_FORMAT_DXT5,
1000 'RGB': NBVTF_IMAGE_FORMAT_RGB888
1001 }[ img.cxr_data.fmt ]
1002
1003 mipmap = img.cxr_data.mipmap
1004 lod = img.cxr_data.lod
1005 clamp = img.cxr_data.clamp
1006 flags = img.cxr_data.flags
1007
1008 q=bpy.context.scene.cxr_data.image_quality
1009
1010 userflag_hash = F"{mipmap}.{lod}.{clamp}.{flags}"
1011 file_hash = F"{name}.{os.path.getmtime(src_path)}"
1012 comphash = F"{file_hash}.{dims[0]}.{dims[1]}.{fmt}.{userflag_hash}.{q}"
1013
1014 if img.cxr_data.last_hash != comphash:
1015 print( F"Texture update: {img.filepath}" )
1016
1017 src = src_path.encode('utf-8')
1018 dst = (asset_full_path('materials',img)+'.vtf').encode('utf-8')
1019
1020 flags_full = flags
1021
1022 # texture setting flags
1023 if not lod: flags_full |= NBVTF_TEXTUREFLAGS_NOLOD
1024 if clamp:
1025 flags_full |= NBVTF_TEXTUREFLAGS_CLAMPS
1026 flags_full |= NBVTF_TEXTUREFLAGS_CLAMPT
1027
1028 if libnbvtf_convert.call(src,dims[0],dims[1],mipmap,fmt,q,flags_full,dst):
1029 img.cxr_data.last_hash = comphash
1030
1031 return name
1032
1033 #
1034 # Compile a material to VMT format. This is quick to run, doesnt need to be a
1035 # job handler.
1036 #
1037 def compile_material(mat):
1038 print( F"Compile {asset_full_path('materials',mat)}.vmt" )
1039
1040 info = material_info(mat)
1041 properties = mat.cxr_data
1042
1043 props = []
1044
1045 # Walk the property tree
1046 def _mlayer( layer ):
1047 nonlocal properties, props
1048
1049 for decl in layer:
1050 if isinstance(layer[decl],dict): # $property definition
1051 pdef = layer[decl]
1052 ptype = pdef['type']
1053
1054 subdefines = False
1055 default = None
1056 prop = None
1057
1058 if 'shaders' in pdef and properties.shader not in pdef['shaders']:
1059 continue
1060
1061 # Group expansion (does it have subdefinitions?)
1062 for ch in pdef:
1063 if isinstance(pdef[ch],dict):
1064 subdefines = True
1065 break
1066
1067 expandview = False
1068
1069 if ptype == 'ui':
1070 expandview = True
1071 else:
1072 if ptype == 'intrinsic':
1073 if decl in info:
1074 prop = info[decl]
1075 else:
1076 prop = getattr(properties,decl)
1077 default = pdef['default']
1078
1079 if not isinstance(prop,str) and \
1080 not isinstance(prop,bpy.types.Image) and \
1081 hasattr(prop,'__getitem__'):
1082 prop = tuple([p for p in prop])
1083
1084 if prop != default:
1085 # write prop
1086 props += [(decl,pdef,prop)]
1087
1088 if subdefines:
1089 expandview = True
1090
1091 if expandview: _mlayer(pdef)
1092
1093 _mlayer( cxr_shader_params )
1094
1095 # Write the vmt
1096 with vdf_structure( F"{asset_full_path('materials',mat)}.vmt" ) as vmt:
1097 vmt.node( properties.shader )
1098 vmt.put( "// Convexer export\n" )
1099
1100 for pair in props:
1101 decl = pair[0]
1102 pdef = pair[1]
1103 prop = pair[2]
1104
1105 def _numeric(v):
1106 nonlocal pdef
1107 if 'exponent' in pdef: return str(pow( v, pdef['exponent'] ))
1108 else: return str(v)
1109
1110 if isinstance(prop,bpy.types.Image):
1111 vmt.kv( decl, asset_name(prop))
1112 elif isinstance(prop,bool):
1113 vmt.kv( decl, '1' if prop else '0' )
1114 elif isinstance(prop,str):
1115 vmt.kv( decl, prop )
1116 elif isinstance(prop,float) or isinstance(prop,int):
1117 vmt.kv( decl, _numeric(prop) )
1118 elif isinstance(prop,tuple):
1119 vmt.kv( decl, F"[{' '.join([_numeric(_) for _ in prop])}]" )
1120 else:
1121 vmt.put( F"// (cxr) unkown shader value type'{type(prop)}'" )
1122
1123 vmt.edon()
1124 return props
1125
1126 # Convexer operators
1127 # ------------------------------------------------------------------------------
1128
1129 # Force reload of shared libraries
1130 #
1131 class CXR_RELOAD(bpy.types.Operator):
1132 bl_idname="convexer.reload"
1133 bl_label="Reload"
1134 def execute(_,context):
1135 shared_reload()
1136 return {'FINISHED'}
1137
1138 # Used for exporting data to use with ASAN builds
1139 #
1140 class CXR_DEV_OPERATOR(bpy.types.Operator):
1141 bl_idname="convexer.dev_test"
1142 bl_label="Export development data"
1143
1144 def execute(_,context):
1145 # Prepare input data
1146 mesh_src = mesh_cxr_format(context.active_object)
1147 libcxr_write_test_data.call( pointer(mesh_src) )
1148 return {'FINISHED'}
1149
1150 # UI: Preview how the brushes will looks in 3D view
1151 #
1152 class CXR_PREVIEW_OPERATOR(bpy.types.Operator):
1153 bl_idname="convexer.preview"
1154 bl_label="Preview Brushes"
1155
1156 LASTERR = None
1157 RUNNING = False
1158
1159 def execute(_,context):
1160 return {'FINISHED'}
1161
1162 def modal(_,context,event):
1163 global cxr_view_mesh
1164 static = _.__class__
1165
1166 if event.type == 'ESC':
1167 cxr_reset_lines()
1168 cxr_batch_lines()
1169 cxr_view_mesh = None
1170 static.RUNNING = False
1171
1172 scene_redraw()
1173 return {'FINISHED'}
1174
1175 return {'PASS_THROUGH'}
1176
1177 def invoke(_,context,event):
1178 global cxr_view_shader, cxr_view_mesh
1179 static = _.__class__
1180 static.LASTERR = None
1181
1182 cxr_reset_lines()
1183
1184 mesh_src = mesh_cxr_format(context.active_object)
1185
1186 err = c_int32(0)
1187 world = libcxr_decompose.call( mesh_src, pointer(err) )
1188
1189 if world == None:
1190 cxr_view_mesh = None
1191 cxr_batch_lines()
1192 scene_redraw()
1193
1194 static.LASTERR = ["There is no error", \
1195 "Non-Manifold",\
1196 "Bad-Manifold",\
1197 "No-Candidate",\
1198 "Internal-Fail",\
1199 "Non-Coplanar",\
1200 "Non-Convex Polygon"]\
1201 [err.value]
1202
1203 if static.RUNNING:
1204 return {'CANCELLED'}
1205 else:
1206 context.window_manager.modal_handler_add(_)
1207 return {'RUNNING_MODAL'}
1208
1209 # Generate preview using cxr
1210 #
1211 ptrpreview = libcxr_world_preview.call( world )
1212 preview = ptrpreview[0]
1213
1214 vertices = preview.vertices[:preview.vertex_count]
1215 vertices = [(_[0],_[1],_[2]) for _ in vertices]
1216
1217 colours = preview.colours[:preview.vertex_count]
1218 colours = [(_[0],_[1],_[2],_[3]) for _ in colours]
1219
1220 indices = preview.indices[:preview.indices_count]
1221 indices = [ (indices[i*3+0],indices[i*3+1],indices[i*3+2]) \
1222 for i in range(int(preview.indices_count/3)) ]
1223
1224 cxr_view_mesh = batch_for_shader(
1225 cxr_view_shader, 'TRIS',
1226 { "pos": vertices, "color": colours },
1227 indices = indices,
1228 )
1229
1230 libcxr_free_tri_mesh.call( ptrpreview )
1231 libcxr_free_world.call( world )
1232 cxr_batch_lines()
1233 scene_redraw()
1234
1235 # Allow user to spam the operator
1236 if static.RUNNING:
1237 return {'CANCELLED'}
1238
1239 if not static.RUNNING:
1240 static.RUNNING = True
1241 context.window_manager.modal_handler_add(_)
1242 return {'RUNNING_MODAL'}
1243
1244 # Search for VMF compiler executables in subdirectory
1245 #
1246 class CXR_DETECT_COMPILERS(bpy.types.Operator):
1247 bl_idname="convexer.detect_compilers"
1248 bl_label="Find compilers"
1249
1250 def execute(self,context):
1251 scene = context.scene
1252 settings = scene.cxr_data
1253 subdir = settings.subdir
1254
1255 for exename in ['studiomdl','vbsp','vvis','vrad']:
1256 searchpath = os.path.normpath(F'{subdir}/../bin/{exename}.exe')
1257 if os.path.exists(searchpath):
1258 settings[F'exe_{exename}'] = searchpath
1259
1260 return {'FINISHED'}
1261
1262 # Main compile function
1263 #
1264 class CXR_COMPILER_CHAIN(bpy.types.Operator):
1265 bl_idname="convexer.chain"
1266 bl_label="Compile Chain"
1267
1268 # 'static'
1269 USER_EXIT = False
1270 SUBPROC = None
1271 TIMER = None
1272 TIMER_LAST = 0.0
1273 WAIT_REDRAW = False
1274
1275 JOBINFO = None
1276 JOBID = 0
1277
1278 def cancel(_,context):
1279 static = _.__class__
1280 wm = context.window_manager
1281
1282 if static.SUBPROC != None:
1283 static.SUBPROC.terminate()
1284 static.SUBPROC = None
1285
1286 if static.TIMER != None:
1287 wm.event_timer_remove( static.TIMER )
1288 static.TIMER = None
1289
1290 scene_redraw()
1291 return {'FINISHED'}
1292
1293 def modal(_,context,ev):
1294 static = _.__class__
1295
1296 if ev.type == 'TIMER':
1297 if static.WAIT_REDRAW:
1298 return {'PASS_THROUGH'}
1299 static.WAIT_REDRAW = True
1300
1301 if static.USER_EXIT:
1302 print( "Chain USER_EXIT" )
1303 return _.cancel(context)
1304
1305 if static.SUBPROC != None:
1306 # Deal with async modes
1307 pass
1308
1309 # Compile syncronous thing
1310 for sys in static.JOBINFO:
1311 for i,target in enumerate(sys['jobs']):
1312 if target != None:
1313 print( F"Start job: {static.JOBID} @{time.time()}" )
1314
1315 if not sys['exec'](target):
1316 print( "Job failed" )
1317 return _.cancel(context)
1318
1319 sys['jobs'][i] = None
1320 static.JOBID += 1
1321
1322 cxr_jobs_update_graph( static.JOBINFO )
1323 scene_redraw()
1324 return {'PASS_THROUGH'}
1325
1326 # All completed
1327 print( "All jobs completed!" )
1328 global cxr_jobs_batch
1329 cxr_jobs_batch = None
1330
1331 scene_redraw()
1332 return _.cancel(context)
1333
1334 return {'PASS_THROUGH'}
1335
1336 def invoke(_,context,event):
1337 static = _.__class__
1338 wm = context.window_manager
1339
1340 if static.TIMER == None:
1341 print("Launching compiler toolchain")
1342
1343 # Run static compilation units now (collect, vmt..)
1344 sceneinfo = cxr_scene_collect()
1345 image_jobs = []
1346
1347 # Collect materials
1348 a_materials = set()
1349 for brush in sceneinfo['geo']:
1350 for ms in brush['object'].material_slots:
1351 a_materials.add( ms.material )
1352
1353 # Collect images
1354 for mat in a_materials:
1355 for pair in compile_material(mat):
1356 decl = pair[0]
1357 pdef = pair[1]
1358 prop = pair[2]
1359
1360 if isinstance(prop,bpy.types.Image):
1361 flags = 0
1362 if 'flags' in pdef: flags = pdef['flags']
1363 if prop not in image_jobs:
1364 image_jobs += [prop]
1365 prop.cxr_data.flags = flags
1366
1367 # Convexer jobs
1368 static.JOBID = 0
1369 static.JOBINFO = []
1370
1371 static.JOBINFO += [{
1372 "title": "Convexer",
1373 "w": 40,
1374 "colour": (1.0,0.3,0.1,1.0),
1375 "exec": cxr_export_vmf,
1376 "jobs": [sceneinfo]
1377 }]
1378
1379 if len(image_jobs) > 0:
1380 static.JOBINFO += [{
1381 "title": "Textures",
1382 "w": 40,
1383 "colour": (0.1,1.0,0.3,1.0),
1384 "exec": compile_image,
1385 "jobs": image_jobs
1386 }]
1387
1388 static.USER_EXIT=False
1389 static.TIMER=wm.event_timer_add(0.1,window=context.window)
1390 wm.modal_handler_add(_)
1391
1392 cxr_jobs_update_graph( static.JOBINFO )
1393 scene_redraw()
1394 return {'RUNNING_MODAL'}
1395
1396 print("Chain exiting...")
1397 static.USER_EXIT=True
1398 return {'RUNNING_MODAL'}
1399
1400 # Convexer panels
1401 # ------------------------------------------------------------------------------
1402
1403 # Helper buttons for 3d toolbox view
1404 #
1405 class CXR_VIEW3D( bpy.types.Panel ):
1406 bl_idname = "VIEW3D_PT_convexer"
1407 bl_label = "Convexer"
1408 bl_space_type = 'VIEW_3D'
1409 bl_region_type = 'UI'
1410 bl_category = "Convexer"
1411
1412 @classmethod
1413 def poll(cls, context):
1414 return (context.object is not None)
1415
1416 def draw(_, context):
1417 layout = _.layout
1418 row = layout.row()
1419 row.scale_y = 2
1420 row.operator("convexer.preview")
1421
1422 if CXR_PREVIEW_OPERATOR.LASTERR != None:
1423 box = layout.box()
1424 box.label(text=CXR_PREVIEW_OPERATOR.LASTERR, icon='ERROR')
1425
1426 # Main scene properties interface, where all the settings go
1427 #
1428 class CXR_INTERFACE(bpy.types.Panel):
1429 bl_label="Convexer"
1430 bl_idname="SCENE_PT_convexer"
1431 bl_space_type='PROPERTIES'
1432 bl_region_type='WINDOW'
1433 bl_context="scene"
1434
1435 def draw(_,context):
1436 _.layout.operator("convexer.reload")
1437 _.layout.operator("convexer.dev_test")
1438 _.layout.operator("convexer.preview")
1439
1440 settings = context.scene.cxr_data
1441
1442 _.layout.prop(settings, "debug")
1443 _.layout.prop(settings, "scale_factor")
1444 _.layout.prop(settings, "lightmap_scale")
1445 _.layout.prop(settings, "light_scale" )
1446 _.layout.prop(settings, "image_quality" )
1447
1448 box = _.layout.box()
1449
1450 box.prop(settings, "project_name")
1451 box.prop(settings, "subdir")
1452
1453 box = _.layout.box()
1454 box.operator("convexer.detect_compilers")
1455 box.prop(settings, "exe_studiomdl")
1456 box.prop(settings, "exe_vbsp")
1457 box.prop(settings, "exe_vvis")
1458 box.prop(settings, "exe_vrad")
1459
1460 text = "Compile" if CXR_COMPILER_CHAIN.TIMER == None else "Cancel"
1461 row = box.row()
1462 row.scale_y = 3
1463 row.operator("convexer.chain", text=text)
1464
1465
1466 class CXR_MATERIAL_PANEL(bpy.types.Panel):
1467 bl_label="VMT Properties"
1468 bl_idname="SCENE_PT_convexer_vmt"
1469 bl_space_type='PROPERTIES'
1470 bl_region_type='WINDOW'
1471 bl_context="material"
1472
1473 def draw(_,context):
1474 active_object = bpy.context.active_object
1475 if active_object == None: return
1476
1477 active_material = active_object.active_material
1478 if active_material == None: return
1479
1480 properties = active_material.cxr_data
1481 info = material_info( active_material )
1482
1483 _.layout.label(text=F"{info['name']} @{info['res'][0]}x{info['res'][1]}")
1484 _.layout.prop( properties, "shader" )
1485
1486 for xk in info:
1487 _.layout.label(text=F"{xk}:={info[xk]}")
1488
1489 def _mtex( name, img, uiParent ):
1490 nonlocal properties
1491
1492 box = uiParent.box()
1493 box.label( text=F'{name} "{img.filepath}"' )
1494 def _p2( x ):
1495 if ((x & (x - 1)) == 0):
1496 return x
1497 closest = 0
1498 closest_diff = 10000000
1499 for i in range(16):
1500 dist = abs((1 << i)-x)
1501 if dist < closest_diff:
1502 closest_diff = dist
1503 closest = i
1504 real = 1 << closest
1505 if x > real:
1506 return 1 << (closest+1)
1507 if x < real:
1508 return 1 << (closest-1)
1509 return x
1510
1511 if img is not None:
1512 row = box.row()
1513 row.prop( img.cxr_data, "export_res" )
1514 row.prop( img.cxr_data, "fmt" )
1515
1516 row = box.row()
1517 row.prop( img.cxr_data, "mipmap" )
1518 row.prop( img.cxr_data, "lod" )
1519 row.prop( img.cxr_data, "clamp" )
1520
1521 img.cxr_data.export_res[0] = _p2( img.cxr_data.export_res[0] )
1522 img.cxr_data.export_res[1] = _p2( img.cxr_data.export_res[1] )
1523
1524 def _mview( layer, uiParent ):
1525 nonlocal properties
1526
1527 for decl in layer:
1528 if isinstance(layer[decl],dict): # $property definition
1529 pdef = layer[decl]
1530 ptype = pdef['type']
1531
1532 thisnode = uiParent
1533 expandview = True
1534 drawthis = True
1535
1536 if ('shaders' in pdef) and \
1537 (properties.shader not in pdef['shaders']):
1538 continue
1539
1540 if ptype == 'intrinsic':
1541 if decl not in info:
1542 drawthis = False
1543
1544 if drawthis:
1545 for ch in pdef:
1546 if isinstance(pdef[ch],dict):
1547 if ptype == 'ui' or ptype == 'intrinsic':
1548 pass
1549 elif getattr(properties,decl) == pdef['default']:
1550 expandview = False
1551
1552 thisnode = uiParent.box()
1553 break
1554
1555 if ptype == 'ui':
1556 thisnode.label( text=decl )
1557 elif ptype == 'intrinsic':
1558 if isinstance(info[decl], bpy.types.Image):
1559 _mtex( decl, info[decl], thisnode )
1560 else:
1561 # hidden intrinsic value.
1562 # Means its a float array or something not an image
1563 thisnode.label(text=F"-- hidden intrinsic '{decl}' --")
1564 else:
1565 thisnode.prop(properties,decl)
1566 if expandview: _mview(pdef,thisnode)
1567
1568 _mview( cxr_shader_params, _.layout )
1569
1570 def cxr_entity_changeclass(_,context):
1571 active_object = context.active_object
1572
1573 # Create ID properties
1574 entdef = None
1575 classname = active_object.cxr_data.classname
1576
1577 if classname in cxr_entities:
1578 entdef = cxr_entities[classname]
1579
1580 kvs = entdef['keyvalues']
1581 if callable(kvs): kvs = kvs(active_object)
1582
1583 for k in kvs:
1584 kv = kvs[k]
1585 key = F'cxrkv_{k}'
1586
1587 if callable(kv) or not isinstance(kv,dict): continue
1588
1589 if key not in active_object:
1590 active_object[key] = kv['default']
1591 id_prop = active_object.id_properties_ui(key)
1592 id_prop.update(default=kv['default'])
1593
1594 class CXR_ENTITY_PANEL(bpy.types.Panel):
1595 bl_label="Entity Config"
1596 bl_idname="SCENE_PT_convexer_entity"
1597 bl_space_type='PROPERTIES'
1598 bl_region_type='WINDOW'
1599 bl_context="object"
1600
1601 def draw(_,context):
1602 active_object = bpy.context.active_object
1603
1604 if active_object == None: return
1605
1606 default_context = cxr_object_context( \
1607 bpy.context.scene.cxr_data.scale_factor, 0.0 )
1608
1609 ecn = cxr_intrinsic_classname( active_object )
1610 classname = cxr_custom_class( active_object )
1611
1612 if ecn == None:
1613 if active_object.type == 'MESH':
1614 _.layout.prop( active_object.cxr_data, 'brushclass' )
1615 else: _.layout.prop( active_object.cxr_data, 'classname' )
1616
1617 if classname == 'NONE':
1618 return
1619 else:
1620 _.layout.label(text=F"<implementation defined ({ecn})>")
1621 _.layout.enabled=False
1622 classname = ecn
1623
1624 kvs = cxr_entity_keyvalues( active_object, default_context, classname )
1625 if kvs != None:
1626 for kv in kvs:
1627 if kv[1]:
1628 _.layout.prop( active_object, F'["cxrkv_{kv[0]}"]', text=kv[0])
1629 else:
1630 row = _.layout.row()
1631 row.enabled = False
1632 row.label( text=F'{kv[0]}: {repr(kv[2])}' )
1633 else:
1634 _.layout.label( text=F"ERROR: NO CLASS DEFINITION" )
1635
1636 class CXR_LIGHT_PANEL(bpy.types.Panel):
1637 bl_label = "Source Settings"
1638 bl_idname = "LIGHT_PT_cxr"
1639 bl_space_type = 'PROPERTIES'
1640 bl_region_type = 'WINDOW'
1641 bl_context = "data"
1642
1643 def draw(self, context):
1644 layout = self.layout
1645 scene = context.scene
1646
1647 active_object = bpy.context.active_object
1648 if active_object == None: return
1649
1650 if active_object.type == 'LIGHT' or \
1651 active_object.type == 'LIGHT_PROBE':
1652
1653 properties = active_object.data.cxr_data
1654
1655 if active_object.type == 'LIGHT':
1656 layout.prop( properties, "realtime" )
1657 elif active_object.type == 'LIGHT_PROBE':
1658 layout.prop( properties, "size" )
1659
1660 # Settings groups
1661 # ------------------------------------------------------------------------------
1662
1663 class CXR_IMAGE_SETTINGS(bpy.types.PropertyGroup):
1664 export_res: bpy.props.IntVectorProperty(
1665 name="",
1666 description="Texture Export Resolution",
1667 default=(512,512),
1668 min=1,
1669 max=4096,
1670 size=2)
1671
1672 fmt: bpy.props.EnumProperty(
1673 name="Format",
1674 items = [
1675 ('DXT1', "DXT1", "BC1 compressed", '', 0),
1676 ('DXT5', "DXT5", "BC3 compressed", '', 1),
1677 ('RGB', "RGB", "Uncompressed", '', 2),
1678 ('RGBA', "RGBA", "Uncompressed (with alpha)", '', 3)
1679 ],
1680 description="Image format",
1681 default=0)
1682
1683 last_hash: bpy.props.StringProperty( name="" )
1684 asset_id: bpy.props.IntProperty(name="intl_assetid",default=0)
1685
1686 mipmap: bpy.props.BoolProperty(name="MIP",default=True)
1687 lod: bpy.props.BoolProperty(name="LOD",default=True)
1688 clamp: bpy.props.BoolProperty(name="CLAMP",default=False)
1689 flags: bpy.props.IntProperty(name="flags",default=0)
1690
1691 class CXR_LIGHT_SETTINGS(bpy.types.PropertyGroup):
1692 realtime: bpy.props.BoolProperty(name="Realtime Light", default=True)
1693
1694 class CXR_CUBEMAP_SETTINGS(bpy.types.PropertyGroup):
1695 size: bpy.props.EnumProperty(
1696 name="Resolution",
1697 items = [
1698 ('1',"1x1",'','',0),
1699 ('2',"2x2",'','',1),
1700 ('3',"4x4",'','',2),
1701 ('4',"8x8",'','',3),
1702 ('5',"16x16",'','',4),
1703 ('6',"32x32",'','',5),
1704 ('7',"64x64",'','',6),
1705 ('8',"128x128",'','',7),
1706 ('9',"256x256",'','',8)
1707 ],
1708 description="Texture resolution",
1709 default=7)
1710
1711 class CXR_ENTITY_SETTINGS(bpy.types.PropertyGroup):
1712 entity: bpy.props.BoolProperty(name="")
1713
1714 enum_pointents = [('NONE',"None","")]
1715 enum_brushents = [('NONE',"None","")]
1716
1717 for classname in cxr_entities:
1718 entdef = cxr_entities[classname]
1719 if 'allow' in entdef:
1720 itm = [(classname, classname, "")]
1721 if 'EMPTY' in entdef['allow']: enum_pointents += itm
1722 else: enum_brushents += itm
1723
1724 classname: bpy.props.EnumProperty(items=enum_pointents, name="Class", \
1725 update=cxr_entity_changeclass, default='NONE' )
1726
1727 brushclass: bpy.props.EnumProperty(items=enum_brushents, name="Class", \
1728 update=cxr_entity_changeclass, default='NONE' )
1729
1730 class CXR_MODEL_SETTINGS(bpy.types.PropertyGroup):
1731 last_hash: bpy.props.StringProperty( name="" )
1732 asset_id: bpy.props.IntProperty(name="vmf_settings",default=0)
1733
1734 class CXR_SCENE_SETTINGS(bpy.types.PropertyGroup):
1735 project_name: bpy.props.StringProperty( name="Project Name" )
1736 subdir: bpy.props.StringProperty( name="Subdirectory" )
1737
1738 exe_studiomdl: bpy.props.StringProperty( name="studiomdl" )
1739 exe_vbsp: bpy.props.StringProperty( name="vbsp" )
1740 opt_vbsp: bpy.props.StringProperty( name="args" )
1741 exe_vvis: bpy.props.StringProperty( name="vvis" )
1742 opt_vvis: bpy.props.StringProperty( name="args" )
1743 exe_vrad: bpy.props.StringProperty( name="vrad" )
1744 opt_vrad: bpy.props.StringProperty( name="args" )
1745
1746 debug: bpy.props.BoolProperty(name="Debug",default=False)
1747 scale_factor: bpy.props.FloatProperty( name="VMF Scale factor", \
1748 default=32.0,min=1.0)
1749 skybox_scale_factor: bpy.props.FloatProperty( name="Sky Scale factor", \
1750 default=1.0,min=0.01)
1751
1752 skybox_offset: bpy.props.FloatProperty(name="Sky offset",default=-4096.0)
1753 light_scale: bpy.props.FloatProperty(name="Light Scale",default=1.0/5.0)
1754 include_names: bpy.props.BoolProperty(name="Append original file names",\
1755 default=True)
1756 lightmap_scale: bpy.props.IntProperty(name="Global Lightmap Scale",\
1757 default=12)
1758 image_quality: bpy.props.IntProperty(name="Texture Quality (0-18)",\
1759 default=8, min=0, max=18 )
1760
1761
1762 classes = [ CXR_RELOAD, CXR_DEV_OPERATOR, CXR_INTERFACE, \
1763 CXR_MATERIAL_PANEL, CXR_IMAGE_SETTINGS,\
1764 CXR_MODEL_SETTINGS, CXR_ENTITY_SETTINGS, CXR_CUBEMAP_SETTINGS,\
1765 CXR_LIGHT_SETTINGS, CXR_SCENE_SETTINGS, CXR_DETECT_COMPILERS,\
1766 CXR_ENTITY_PANEL, CXR_LIGHT_PANEL, CXR_PREVIEW_OPERATOR,\
1767 CXR_VIEW3D, CXR_COMPILER_CHAIN ]
1768
1769 vmt_param_dynamic_class = None
1770
1771 def register():
1772 global cxr_view_draw_handler, vmt_param_dynamic_class, cxr_ui_handler
1773
1774 for c in classes:
1775 bpy.utils.register_class(c)
1776
1777 # Build dynamic VMT properties class defined by cxr_shader_params
1778 annotations_dict = {}
1779
1780 def _dvmt_propogate(layer):
1781 nonlocal annotations_dict
1782
1783 for decl in layer:
1784 if isinstance(layer[decl],dict): # $property definition
1785 pdef = layer[decl]
1786
1787 prop = None
1788 if pdef['type'] == 'bool':
1789 prop = bpy.props.BoolProperty(\
1790 name = pdef['name'],\
1791 default = pdef['default'])
1792
1793 elif pdef['type'] == 'float':
1794 prop = bpy.props.FloatProperty(\
1795 name = pdef['name'],\
1796 default = pdef['default'])
1797
1798 elif pdef['type'] == 'vector':
1799 if 'subtype' in pdef:
1800 prop = bpy.props.FloatVectorProperty(\
1801 name = pdef['name'],\
1802 subtype = pdef['subtype'],\
1803 default = pdef['default'],\
1804 size = len(pdef['default']))
1805 else:
1806 prop = bpy.props.FloatVectorProperty(\
1807 name = pdef['name'],\
1808 default = pdef['default'],\
1809 size = len(pdef['default']))
1810
1811 elif pdef['type'] == 'string':
1812 prop = bpy.props.StringProperty(\
1813 name = pdef['name'],\
1814 default = pdef['default'])
1815
1816 elif pdef['type'] == 'enum':
1817 prop = bpy.props.EnumProperty(\
1818 name = pdef['name'],\
1819 items = pdef['items'],\
1820 default = pdef['default'])
1821
1822 if prop != None:
1823 annotations_dict[decl] = prop
1824
1825 # Recurse into sub-definitions
1826 _dvmt_propogate(pdef)
1827
1828 annotations_dict["shader"] = bpy.props.EnumProperty(\
1829 name = "Shader",\
1830 items = [( _,\
1831 cxr_shaders[_]["name"],\
1832 '') for _ in cxr_shaders],\
1833 default = next(iter(cxr_shaders)))
1834
1835 annotations_dict["asset_id"] = bpy.props.IntProperty(name="intl_assetid",\
1836 default=0)
1837
1838 _dvmt_propogate( cxr_shader_params )
1839 vmt_param_dynamic_class = type(
1840 "CXR_VMT_DYNAMIC",
1841 (bpy.types.PropertyGroup,),{
1842 "__annotations__": annotations_dict
1843 },
1844 )
1845
1846 bpy.utils.register_class( vmt_param_dynamic_class )
1847
1848 # Pointer types
1849 bpy.types.Material.cxr_data = \
1850 bpy.props.PointerProperty(type=vmt_param_dynamic_class)
1851 bpy.types.Image.cxr_data = \
1852 bpy.props.PointerProperty(type=CXR_IMAGE_SETTINGS)
1853 bpy.types.Object.cxr_data = \
1854 bpy.props.PointerProperty(type=CXR_ENTITY_SETTINGS)
1855 bpy.types.Collection.cxr_data = \
1856 bpy.props.PointerProperty(type=CXR_MODEL_SETTINGS)
1857 bpy.types.Light.cxr_data = \
1858 bpy.props.PointerProperty(type=CXR_LIGHT_SETTINGS)
1859 bpy.types.LightProbe.cxr_data = \
1860 bpy.props.PointerProperty(type=CXR_CUBEMAP_SETTINGS)
1861 bpy.types.Scene.cxr_data = \
1862 bpy.props.PointerProperty(type=CXR_SCENE_SETTINGS)
1863
1864 # CXR Scene settings
1865
1866 # GPU / callbacks
1867 cxr_view_draw_handler = bpy.types.SpaceView3D.draw_handler_add(\
1868 cxr_draw,(),'WINDOW','POST_VIEW')
1869
1870 cxr_ui_handler = bpy.types.SpaceView3D.draw_handler_add(\
1871 cxr_ui,(None,None),'WINDOW','POST_PIXEL')
1872
1873 bpy.app.handlers.load_post.append(cxr_on_load)
1874 bpy.app.handlers.depsgraph_update_post.append(cxr_dgraph_update)
1875
1876 def unregister():
1877 global cxr_view_draw_handler, vmt_param_dynamic_class, cxr_ui_handler
1878
1879 bpy.utils.unregister_class( vmt_param_dynamic_class )
1880 for c in classes:
1881 bpy.utils.unregister_class(c)
1882
1883 bpy.app.handlers.depsgraph_update_post.remove(cxr_dgraph_update)
1884 bpy.app.handlers.load_post.remove(cxr_on_load)
1885
1886 bpy.types.SpaceView3D.draw_handler_remove(cxr_view_draw_handler,'WINDOW')
1887 bpy.types.SpaceView3D.draw_handler_remove(cxr_ui_handler,'WINDOW')