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