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