surface props
[carveJwlIkooP6JGAAIwe30JlM.git] / blender_export.py
1 #
2 # =============================================================================
3 #
4 # Copyright . . . -----, ,----- ,---. .---.
5 # 2021-2022 |\ /| | / | | | | /|
6 # | \ / | +-- / +----- +---' | / |
7 # | \ / | | / | | \ | / |
8 # | \/ | | / | | \ | / |
9 # ' ' '--' [] '----- '----- ' ' '---' SOFTWARE
10 #
11 # =============================================================================
12 #
13 # Python exporter for Blender, compiles .mdl format for Skate Rift.
14 #
15 # Its really slow, sorry, I don't know how to speed it up.
16 # Also not sure why you need to put # before {} in code blocks, there is errors
17 # otherwise
18 #
19
20 import bpy, math, gpu, os
21 import cProfile
22 from ctypes import *
23 from mathutils import *
24 from gpu_extras.batch import batch_for_shader
25
26 bl_info = {
27 "name":"Skate Rift model compiler",
28 "author": "Harry Godden (hgn)",
29 "version": (0,2),
30 "blender":(3,1,0),
31 "location":"Export",
32 "descriptin":"",
33 "warning":"",
34 "wiki_url":"",
35 "category":"Import/Export",
36 }
37
38 class mdl_vert(Structure): # 48 bytes. Quite large. Could compress
39 #{ # the normals and uvs to i16s. Not an
40 _pack_ = 1 # real issue, yet.
41 _fields_ = [("co",c_float*3),
42 ("norm",c_float*3),
43 ("uv",c_float*2),
44 ("colour",c_uint8*4),
45 ("weights",c_uint16*4),
46 ("groups",c_uint8*4)]
47 #}
48
49 class mdl_submesh(Structure):
50 #{
51 _pack_ = 1
52 _fields_ = [("indice_start",c_uint32),
53 ("indice_count",c_uint32),
54 ("vertex_start",c_uint32),
55 ("vertex_count",c_uint32),
56 ("bbx",(c_float*3)*2),
57 ("material_id",c_uint32)] # index into the material array
58 #}
59
60 class mdl_texture(Structure):
61 #{
62 _pack_ = 1
63 _fields_ = [("pstr_name",c_uint32),
64 ("pack_offset",c_uint32),
65 ("pack_length",c_uint32)]
66 #}
67
68 class mdl_material(Structure):
69 #{
70 _pack_ = 1
71 _fields_ = [("pstr_name",c_uint32),
72 ("shader",c_uint32),
73 ("flags",c_uint32),
74 ("surface_prop",c_uint32),
75 ("colour",c_float*4),
76 ("colour1",c_float*4),
77 ("tex_diffuse",c_uint32),
78 ("tex_decal",c_uint32),
79 ("tex_normal",c_uint32)]
80 #}
81
82 class mdl_node(Structure):
83 #{
84 _pack_ = 1
85 _fields_ = [("co",c_float*3),
86 ( "q",c_float*4),
87 ( "s",c_float*3),
88 ("sub_uid",c_uint32), # dont use
89 ("submesh_start",c_uint32),
90 ("submesh_count",c_uint32),
91 ("classtype",c_uint32),
92 ("offset",c_uint32),
93 ("parent",c_uint32),
94 ("pstr_name",c_uint32)]
95 #}
96
97 class mdl_header(Structure):
98 #{
99 _pack_ = 1
100 _fields_ = [("identifier",c_uint32),
101 ("version",c_uint32),
102 ("file_length",c_uint32),
103 ("pad0",c_uint32),
104
105 ("node_count",c_uint32),
106 ("node_offset",c_uint32),
107
108 ("submesh_count",c_uint32),
109 ("submesh_offset",c_uint32),
110
111 ("material_count",c_uint32),
112 ("material_offset",c_uint32),
113
114 ("texture_count",c_uint32),
115 ("texture_offset",c_uint32),
116
117 ("anim_count",c_uint32),
118 ("anim_offset",c_uint32),
119
120 ("entdata_size",c_uint32),
121 ("entdata_offset",c_uint32),
122
123 ("strings_size",c_uint32),
124 ("strings_offset",c_uint32),
125
126 ("keyframe_count",c_uint32),
127 ("keyframe_offset",c_uint32),
128
129 ("vertex_count",c_uint32),
130 ("vertex_offset",c_uint32),
131
132 ("indice_count",c_uint32),
133 ("indice_offset",c_uint32),
134
135 ("pack_size",c_uint32),
136 ("pack_offset",c_uint32)]
137 #}
138
139 class mdl_animation(Structure):
140 #{
141 _pack_ = 1
142 _fields_ = [("pstr_name",c_uint32),
143 ("length",c_uint32),
144 ("rate",c_float),
145 ("offset",c_uint32)]
146 #}
147
148 class mdl_keyframe(Structure):
149 #{
150 _pack_ = 1
151 _fields_ = [("co",c_float*3),
152 ("q",c_float*4),
153 ("s",c_float*3)]
154 #}
155
156 # ---------------------------------------------------------------------------- #
157 # #
158 # Entity definitions #
159 # #
160 # ---------------------------------------------------------------------------- #
161 #
162 # ctypes _fields_ defines the data which is filled in by:
163 # def encode_obj( _, node, node_def ):
164 #
165 # gizmos get drawn into the viewport via:
166 # @staticmethod
167 # def draw_scene_helpers( obj ):
168 #
169 # editor enterface, simiraliy:
170 # @staticmethod
171 # def editor_interface( layout, obj ):
172 #
173
174 # Classtype 1
175 #
176 # Purpose: A rift. must target another gate, the target gate can not have more
177 # than one target nodes of its own.
178 #
179 class classtype_gate(Structure):
180 #{
181 _pack_ = 1
182 _fields_ = [("target",c_uint32),
183 ("dims",c_float*3)]
184
185 def encode_obj(_, node,node_def):
186 #{
187 node.classtype = 1
188
189 obj = node_def['obj']
190
191 if obj.cv_data.target != None:
192 _.target = obj.cv_data.target.cv_data.uid
193
194 if obj.type == 'MESH':
195 #{
196 _.dims[0] = obj.data.cv_data.v0[0]
197 _.dims[1] = obj.data.cv_data.v0[1]
198 _.dims[2] = obj.data.cv_data.v0[2]
199 #}
200 else:
201 #{
202 _.dims[0] = obj.cv_data.v0[0]
203 _.dims[1] = obj.cv_data.v0[1]
204 _.dims[2] = obj.cv_data.v0[2]
205 #}
206 #}
207
208 @staticmethod
209 def draw_scene_helpers( obj ):
210 #{
211 global cv_view_verts, cv_view_colours
212
213 if obj.type == 'MESH':
214 dims = obj.data.cv_data.v0
215 else:
216 dims = obj.cv_data.v0
217
218 vs = [None]*9
219 c = Vector((0,0,dims[2]))
220
221 vs[0] = obj.matrix_world @ Vector((-dims[0],0.0,-dims[1]+dims[2]))
222 vs[1] = obj.matrix_world @ Vector((-dims[0],0.0, dims[1]+dims[2]))
223 vs[2] = obj.matrix_world @ Vector(( dims[0],0.0, dims[1]+dims[2]))
224 vs[3] = obj.matrix_world @ Vector(( dims[0],0.0,-dims[1]+dims[2]))
225 vs[4] = obj.matrix_world @ (c+Vector((-1,0,-2)))
226 vs[5] = obj.matrix_world @ (c+Vector((-1,0, 2)))
227 vs[6] = obj.matrix_world @ (c+Vector(( 1,0, 2)))
228 vs[7] = obj.matrix_world @ (c+Vector((-1,0, 0)))
229 vs[8] = obj.matrix_world @ (c+Vector(( 1,0, 0)))
230
231 indices = [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(7,8)]
232
233 for l in indices:
234 #{
235 v0 = vs[l[0]]
236 v1 = vs[l[1]]
237 cv_view_verts += [(v0[0],v0[1],v0[2])]
238 cv_view_verts += [(v1[0],v1[1],v1[2])]
239 cv_view_colours += [(1,1,0,1),(1,1,0,1)]
240 #}
241
242 sw = (0.4,0.4,0.4,0.2)
243 if obj.cv_data.target != None:
244 cv_draw_arrow( obj.location, obj.cv_data.target.location, sw )
245 #}
246
247 @staticmethod
248 def editor_interface( layout, obj ):
249 #{
250 layout.prop( obj.cv_data, "target" )
251
252 mesh = obj.data
253 layout.label( text=F"(i) Data is stored in {mesh.name}" )
254 layout.prop( mesh.cv_data, "v0", text="Gate dimensions" )
255 #}
256 #}
257
258 # Classtype 3
259 #
260 # Purpose: player can reset here, its a safe place
261 # spawns can share the same name, the closest one will be picked
262 #
263 # when the world loads it will pick the one named 'start' first.
264 #
265 class classtype_spawn(Structure):
266 #{
267 _pack_ = 1
268 _fields_ = [("pstr_alias",c_uint32)]
269
270 def encode_obj(_, node,node_def):
271 #{
272 node.classtype = 3
273 _.pstr_alias = encoder_process_pstr( node_def['obj'].cv_data.strp )
274 #}
275
276 @staticmethod
277 def draw_scene_helpers( obj ):
278 #{
279 global cv_view_verts, cv_view_colours
280
281 vs = [None]*4
282 vs[0] = obj.matrix_world @ Vector((0,0,0))
283 vs[1] = obj.matrix_world @ Vector((0,2,0))
284 vs[2] = obj.matrix_world @ Vector((0.5,1,0))
285 vs[3] = obj.matrix_world @ Vector((-0.5,1,0))
286 indices = [(0,1),(1,2),(1,3)]
287
288 for l in indices:
289 #{
290 v0 = vs[l[0]]
291 v1 = vs[l[1]]
292
293 cv_view_verts += [(v0[0],v0[1],v0[2])]
294 cv_view_verts += [(v1[0],v1[1],v1[2])]
295 cv_view_colours += [(0,1,1,1),(0,1,1,1)]
296 #}
297 #}
298
299 @staticmethod
300 def editor_interface( layout, obj ):
301 #{
302 layout.prop( obj.cv_data, "strp", text="Alias" )
303 #}
304 #}
305
306 # Classtype 4
307 #
308 # Purpose: Tells the game to draw water HERE, at this entity.
309 #
310 class classtype_water(Structure):
311 #{
312 _pack_ = 1
313 _fields_ = [("temp",c_uint32)]
314
315 def encode_obj(_, node,node_def):
316 #{
317 node.classtype = 4
318 # no data, spooky
319 #}
320 #}
321
322 # Classtype 8
323 #
324 # Purpose: Defines a route node and links to up to two more nodes
325 #
326 class classtype_route_node(Structure):
327 #{
328 _pack_ = 1
329 _fields_ = [("target",c_uint32),
330 ("target1",c_uint32)]
331
332 def encode_obj(_, node,node_def):
333 #{
334 node.classtype = 8
335 obj = node_def['obj']
336
337 if obj.cv_data.target != None:
338 _.target = obj.cv_data.target.cv_data.uid
339 if obj.cv_data.target1 != None:
340 _.target1 = obj.cv_data.target1.cv_data.uid
341 #}
342
343 @staticmethod
344 def draw_scene_helpers( obj ):
345 #{
346 global cv_view_verts, cv_view_colours
347
348 sw = Vector((0.4,0.4,0.4,0.2))
349 sw2 = Vector((1.5,0.2,0.2,0.0))
350 if obj.cv_data.target != None:
351 cv_draw_bpath( obj, obj.cv_data.target, sw, sw )
352 if obj.cv_data.target1 != None:
353 cv_draw_bpath( obj, obj.cv_data.target1, sw, sw )
354
355 cv_draw_bhandle( obj, 1.0, (0.8,0.8,0.8,1.0) )
356 cv_draw_bhandle( obj, -1.0, (0.4,0.4,0.4,1.0) )
357
358 p1 = obj.location+ \
359 obj.matrix_world.to_quaternion() @ Vector((0,0,-6+1.5))
360 cv_draw_arrow( obj.location, p1, sw )
361 #}
362
363 @staticmethod
364 def editor_interface( layout, obj ):
365 #{
366 layout.prop( obj.cv_data, "target", text="Left" )
367 layout.prop( obj.cv_data, "target1", text="Right" )
368 #}
369 #}
370
371 # Classtype 9
372 #
373 # Purpose: Defines a route, its 'starting' point, and the colour to use for it
374 #
375 class classtype_route(Structure):
376 #{
377 _pack_ = 1
378 _fields_ = [("id_start",c_uint32),
379 ("pstr_name",c_uint32),
380 ("colour",c_float*3)]
381
382 def encode_obj(_, node,node_def):
383 #{
384 node.classtype = 9
385 obj = node_def['obj']
386
387 _.colour[0] = obj.cv_data.colour[0]
388 _.colour[1] = obj.cv_data.colour[1]
389 _.colour[2] = obj.cv_data.colour[2]
390 _.pstr_name = encoder_process_pstr( obj.cv_data.strp )
391
392 if obj.cv_data.target != None:
393 _.id_start = obj.cv_data.target.cv_data.uid
394 #}
395
396 @staticmethod
397 def draw_scene_helpers( obj ):
398 #{
399 global cv_view_verts, cv_view_colours, cv_view_course_i
400
401 if obj.cv_data.target:
402 cv_draw_arrow( obj.location, obj.cv_data.target.location, [1,1,1,1] )
403
404 # Tries to simulate how we do it in the game
405 #
406 stack = [None]*64
407 stack_i = [0]*64
408 stack[0] = obj.cv_data.target
409 si = 1
410 loop_complete = False
411
412 while si > 0:
413 #{
414 if stack_i[si-1] == 2:
415 #{
416 si -= 1
417 continue
418
419 if si == 0: # Loop failed to complete
420 break
421 #}
422
423 node = stack[si-1]
424
425 targets = [None,None]
426 targets[0] = node.cv_data.target
427
428 if node.cv_data.classtype == 'classtype_route_node':
429 #{
430 targets[1] = node.cv_data.target1
431 #}
432
433 nextnode = targets[stack_i[si-1]]
434 stack_i[si-1] += 1
435
436 if nextnode != None: # branch
437 #{
438 if nextnode == stack[0]: # Loop completed
439 #{
440 loop_complete = True
441 break
442 #}
443
444 valid=True
445 for sj in range(si):
446 #{
447 if stack[sj] == nextnode: # invalidated path
448 #{
449 valid=False
450 break
451 #}
452 #}
453
454 if valid:
455 #{
456 stack_i[si] = 0
457 stack[si] = nextnode
458 si += 1
459 continue
460 #}
461 #}
462 #}
463
464 if loop_complete:
465 #{
466 cc = Vector((obj.cv_data.colour[0],\
467 obj.cv_data.colour[1],\
468 obj.cv_data.colour[2],\
469 1.0))
470
471 for sj in range(si):
472 #{
473 sk = (sj+1)%si
474
475 if stack[sj].cv_data.classtype == 'classtype_gate' and \
476 stack[sk].cv_data.classtype == 'classtype_gate':
477 #{
478 dist = (stack[sj].location-stack[sk].location).magnitude
479 cv_draw_sbpath( stack[sj], stack[sk], cc*0.4, cc, dist, dist )
480 #}
481 else:
482 cv_draw_bpath( stack[sj], stack[sk], cc, cc )
483 #}
484
485 cv_view_course_i += 1
486 #}
487 #}
488
489 @staticmethod
490 def editor_interface( layout, obj ):
491 #{
492 layout.prop( obj.cv_data, "target", text="'Start' from" )
493 layout.prop( obj.cv_data, "colour" )
494 layout.prop( obj.cv_data, "strp", text="Name" )
495 #}
496 #}
497
498 # Classtype 12
499 #
500 # Purpose: links an mesh node to a type 11
501 #
502 class classtype_skin(Structure):
503 #{
504 _pack_ = 1
505 _fields_ = [("skeleton",c_uint32)]
506
507 def encode_obj(_, node,node_def):
508 #{
509 node.classtype = 12
510
511 armature_def = node_def['linked_armature']
512 _.skeleton = armature_def['obj'].cv_data.uid
513 #}
514 #}
515
516 # Classtype 11
517 #
518 # Purpose: defines the allocation requirements for a skeleton
519 #
520 class classtype_skeleton(Structure):
521 #{
522 _pack_ = 1
523 _fields_ = [("channels",c_uint32),
524 ("ik_count",c_uint32),
525 ("collider_count",c_uint32),
526 ("anim_start",c_uint32),
527 ("anim_count",c_uint32)]
528
529 def encode_obj(_, node,node_def):
530 #{
531 node.classtype = 11
532
533 _.channels = len( node_def['bones'] )
534 _.ik_count = node_def['ik_count']
535 _.collider_count = node_def['collider_count']
536 _.anim_start = node_def['anim_start']
537 _.anim_count = node_def['anim_count']
538 #}
539 #}
540
541
542 # Classtype 10
543 #
544 # Purpose: intrinsic bone type, stores collision information and limits too
545 #
546 class classtype_bone(Structure):
547 #{
548 _pack_ = 1
549 _fields_ = [("deform",c_uint32),
550 ("ik_target",c_uint32),
551 ("ik_pole",c_uint32),
552 ("collider",c_uint32),
553 ("use_limits",c_uint32),
554 ("angle_limits",(c_float*3)*2),
555 ("hitbox",(c_float*3)*2)]
556
557 def encode_obj(_, node,node_def):
558 #{
559 node.classtype = 10
560
561 armature_def = node_def['linked_armature']
562 obj = node_def['bone']
563
564 _.deform = node_def['deform']
565
566 if 'ik_target' in node_def:
567 #{
568 _.ik_target = armature_def['bones'].index( node_def['ik_target'] )
569 _.ik_pole = armature_def['bones'].index( node_def['ik_pole'] )
570 #}
571
572 # For ragdolls
573 #
574 if obj.cv_data.collider:
575 #{
576 _.collider = 1
577 _.hitbox[0][0] = obj.cv_data.v0[0]
578 _.hitbox[0][1] = obj.cv_data.v0[2]
579 _.hitbox[0][2] = -obj.cv_data.v1[1]
580 _.hitbox[1][0] = obj.cv_data.v1[0]
581 _.hitbox[1][1] = obj.cv_data.v1[2]
582 _.hitbox[1][2] = -obj.cv_data.v0[1]
583 #}
584
585 if obj.cv_data.con0:
586 #{
587 _.use_limits = 1
588 _.angle_limits[0][0] = obj.cv_data.mins[0]
589 _.angle_limits[0][1] = obj.cv_data.mins[2]
590 _.angle_limits[0][2] = -obj.cv_data.maxs[1]
591 _.angle_limits[1][0] = obj.cv_data.maxs[0]
592 _.angle_limits[1][1] = obj.cv_data.maxs[2]
593 _.angle_limits[1][2] = -obj.cv_data.mins[1]
594 #}
595 #}
596 #}
597
598 # Classtype 100
599 #
600 # Purpose: sends a signal to another entity
601 #
602 class classtype_trigger(Structure):
603 #{
604 _pack_ = 1
605 _fields_ = [("target",c_uint32)]
606
607 def encode_obj(_, node,node_def ):
608 #{
609 node.classtype = 100
610 if node_def['obj'].cv_data.target:
611 _.target = node_def['obj'].cv_data.target.cv_data.uid
612 #}
613
614 @staticmethod
615 def draw_scene_helpers( obj ):
616 #{
617 global cv_view_verts, cv_view_colours
618 cv_draw_ucube( obj.matrix_world, [0,1,0,1] )
619
620 if obj.cv_data.target:
621 cv_draw_arrow( obj.location, obj.cv_data.target.location, [1,1,1,1] )
622 #}
623
624 @staticmethod
625 def editor_interface( layout, obj ):
626 #{
627 layout.prop( obj.cv_data, "target", text="Triggers" )
628 #}
629 #}
630
631 # Classtype 101
632 #
633 # Purpose: Gives the player an achievement.
634 # No cheating! You shouldn't use this entity anyway, since only ME can
635 # add achievements to the steam ;)
636 #
637 class classtype_logic_achievement(Structure):
638 #{
639 _pack_ = 1
640 _fields_ = [("pstr_name",c_uint32)]
641
642 def encode_obj(_, node,node_def ):
643 #{
644 node.classtype = 101
645 _.pstr_name = encoder_process_pstr( node_def['obj'].cv_data.strp )
646 #}
647
648 @staticmethod
649 def editor_interface( layout, obj ):
650 #{
651 layout.prop( obj.cv_data, "strp", text="Achievement ID" )
652 #}
653 #}
654
655 # Classtype 102
656 #
657 # Purpose: sends a signal to another entity
658 #
659 class classtype_logic_relay(Structure):
660 #{
661 _pack_ = 1
662 _fields_ = [("targets",c_uint32*4)]
663
664 def encode_obj(_, node,node_def ):
665 #{
666 node.classtype = 102
667 obj = node_def['obj']
668 if obj.cv_data.target:
669 _.targets[0] = obj.cv_data.target.cv_data.uid
670 if obj.cv_data.target1:
671 _.targets[1] = obj.cv_data.target1.cv_data.uid
672 if obj.cv_data.target2:
673 _.targets[2] = obj.cv_data.target2.cv_data.uid
674 if obj.cv_data.target3:
675 _.targets[3] = obj.cv_data.target3.cv_data.uid
676 #}
677
678 @staticmethod
679 def draw_scene_helpers( obj ):
680 #{
681 global cv_view_verts, cv_view_colours
682
683 if obj.cv_data.target:
684 cv_draw_arrow( obj.location, obj.cv_data.target.location, [1,1,1,1] )
685 if obj.cv_data.target1:
686 cv_draw_arrow( obj.location, obj.cv_data.target1.location, [1,1,1,1] )
687 if obj.cv_data.target2:
688 cv_draw_arrow( obj.location, obj.cv_data.target2.location, [1,1,1,1] )
689 if obj.cv_data.target3:
690 cv_draw_arrow( obj.location, obj.cv_data.target3.location, [1,1,1,1] )
691 #}
692
693 @staticmethod
694 def editor_interface( layout, obj ):
695 #{
696 layout.prop( obj.cv_data, "target", text="Triggers" )
697 layout.prop( obj.cv_data, "target1", text="Triggers" )
698 layout.prop( obj.cv_data, "target2", text="Triggers" )
699 layout.prop( obj.cv_data, "target3", text="Triggers" )
700 #}
701 #}
702
703 # Classtype 14
704 #
705 # Purpose: Plays some audio (44100hz .ogg vorbis only)
706 # NOTE: There is a 32mb limit on the audio buffer, world audio is
707 # decompressed and stored in signed 16 bit integers (2 bytes)
708 # per sample.
709 #
710 # volume: not used if has 3D flag
711 # flags:
712 # AUDIO_FLAG_LOOP 0x1
713 # AUDIO_FLAG_ONESHOT 0x2 (DONT USE THIS, it breaks semaphores)
714 # AUDIO_FLAG_SPACIAL_3D 0x4 (Probably what you want)
715 # AUDIO_FLAG_AUTO_START 0x8 (Play when the world starts)
716 # ......
717 # the rest are just internal flags, only use the above 3.
718 #
719 class classtype_audio(Structure):
720 #{
721 _pack_ = 1
722 _fields_ = [("pstr_file",c_uint32),
723 ("flags",c_uint32),
724 ("volume",c_float)]
725
726 def encode_obj(_, node,node_def ):
727 #{
728 node.classtype = 14
729
730 obj = node_def['obj']
731
732 _.pstr_file = encoder_process_pstr( obj.cv_data.strp )
733
734 flags = 0x00
735 if obj.cv_data.bp0: flags |= 0x1
736 if obj.cv_data.bp1: flags |= 0x4
737 if obj.cv_data.bp2: flags |= 0x8
738
739 _.flags = flags
740 _.volume = obj.cv_data.fltp
741 #}
742
743 @staticmethod
744 def editor_interface( layout, obj ):
745 #{
746 layout.prop( obj.cv_data, "strp" )
747
748 layout.prop( obj.cv_data, "bp0", text = "Looping" )
749 layout.prop( obj.cv_data, "bp1", text = "3D Audio" )
750 layout.prop( obj.cv_data, "bp2", text = "Auto Start" )
751 #}
752
753 @staticmethod
754 def draw_scene_helpers( obj ):
755 #{
756 global cv_view_verts, cv_view_colours
757
758 cv_draw_sphere( obj.location, obj.scale[0], [1,1,0,1] )
759 #}
760 #}
761
762 # ---------------------------------------------------------------------------- #
763 # #
764 # Compiler section #
765 # #
766 # ---------------------------------------------------------------------------- #
767
768 # Current encoder state
769 #
770 g_encoder = None
771
772 # Reset encoder
773 #
774 def encoder_init( collection ):
775 #{
776 global g_encoder
777
778 g_encoder = \
779 {
780 # The actual file header
781 #
782 'header': mdl_header(),
783
784 # Options
785 #
786 'pack_textures': collection.cv_data.pack_textures,
787
788 # Compiled data chunks (each can be read optionally by the client)
789 #
790 'data':
791 {
792 #1---------------------------------
793 'node': [], # Metadata 'chunk'
794 'submesh': [],
795 'material': [],
796 'texture': [],
797 'anim': [],
798 'entdata': bytearray(), # variable width
799 'strings': bytearray(), # .
800 #2---------------------------------
801 'keyframe': [], # Animations
802 #3---------------------------------
803 'vertex': [], # Mesh data
804 'indice': [],
805 #4---------------------------------
806 'pack': bytearray() # Other generic packed data
807 },
808
809 # All objects of the model in their final heirachy
810 #
811 "uid_count": 1,
812 "scene_graph":{},
813 "graph_lookup":{},
814
815 # Allows us to reuse definitions
816 #
817 'string_cache':{},
818 'mesh_cache': {},
819 'material_cache': {},
820 'texture_cache': {}
821 }
822
823 g_encoder['header'].identifier = 0xABCD0000
824 g_encoder['header'].version = 1
825
826 # Add fake NoneID material and texture
827 #
828 none_material = mdl_material()
829 none_material.pstr_name = encoder_process_pstr( "" )
830 none_material.texture_id = 0
831
832 none_texture = mdl_texture()
833 none_texture.pstr_name = encoder_process_pstr( "" )
834 none_texture.pack_offset = 0
835 none_texture.pack_length = 0
836
837 g_encoder['data']['material'] += [none_material]
838 g_encoder['data']['texture'] += [none_texture]
839
840 g_encoder['data']['pack'].extend( b'datapack\0\0\0\0\0\0\0\0' )
841
842 # Add root node
843 #
844 root = mdl_node()
845 root.co[0] = 0
846 root.co[1] = 0
847 root.co[2] = 0
848 root.q[0] = 0
849 root.q[1] = 0
850 root.q[2] = 0
851 root.q[3] = 1
852 root.s[0] = 1
853 root.s[1] = 1
854 root.s[2] = 1
855 root.pstr_name = encoder_process_pstr('')
856 root.submesh_start = 0
857 root.submesh_count = 0
858 root.offset = 0
859 root.classtype = 0
860 root.parent = 0xffffffff
861
862 g_encoder['data']['node'] += [root]
863 #}
864
865
866 # fill with 0x00 until a multiple of align. Returns how many bytes it added
867 #
868 def bytearray_align_to( buffer, align, offset=0 ):
869 #{
870 count = 0
871
872 while ((len(buffer)+offset) % align) != 0:
873 #{
874 buffer.extend( b'\0' )
875 count += 1
876 #}
877
878 return count
879 #}
880
881 # Add a string to the string buffer except if it already exists there then we
882 # just return its ID.
883 #
884 def encoder_process_pstr( s ):
885 #{
886 global g_encoder
887
888 cache = g_encoder['string_cache']
889
890 if s in cache:
891 return cache[s]
892
893 cache[s] = len( g_encoder['data']['strings'] )
894
895 buffer = g_encoder['data']['strings']
896 buffer.extend( s.encode('utf-8') )
897 buffer.extend( b'\0' )
898
899 bytearray_align_to( buffer, 4 )
900 return cache[s]
901 #}
902
903 def get_texture_resource_name( img ):
904 #{
905 return os.path.splitext( img.name )[0]
906 #}
907
908 # Pack a texture
909 #
910 def encoder_process_texture( img ):
911 #{
912 global g_encoder
913
914 if img == None:
915 return 0
916
917 cache = g_encoder['texture_cache']
918 buffer = g_encoder['data']['texture']
919 pack = g_encoder['data']['pack']
920
921 name = get_texture_resource_name( img )
922
923 if name in cache:
924 return cache[name]
925
926 cache[name] = len( buffer )
927
928 tex = mdl_texture()
929 tex.pstr_name = encoder_process_pstr( name )
930
931 if g_encoder['pack_textures']:
932 #{
933 tex.pack_offset = len( pack )
934 pack.extend( qoi_encode( img ) )
935 tex.pack_length = len( pack ) - tex.pack_offset
936 #}
937 else:
938 tex.pack_offset = 0
939
940 buffer += [ tex ]
941 return cache[name]
942 #}
943
944 def material_tex_image(v):
945 #{
946 return {
947 "Image Texture":
948 {
949 "image": F"{v}"
950 }
951 }
952 #}
953
954 cxr_graph_mapping = \
955 {
956 # Default shader setup
957 "Principled BSDF":
958 {
959 "Base Color":
960 {
961 "Image Texture":
962 {
963 "image": "tex_diffuse"
964 },
965 "Mix":
966 {
967 "Color1": material_tex_image("tex_diffuse"),
968 "Color2": material_tex_image("tex_decal")
969 },
970 },
971 "Normal":
972 {
973 "Normal Map":
974 {
975 "Color": material_tex_image("tex_normal")
976 }
977 }
978 }
979 }
980
981 # https://harrygodden.com/git/?p=convexer.git;a=blob;f=__init__.py;#l1164
982 #
983 def material_info(mat):
984 #{
985 info = {}
986
987 # Using the cv_graph_mapping as a reference, go through the shader
988 # graph and gather all $props from it.
989 #
990 def _graph_read( node_def, node=None, depth=0 ):
991 #{
992 nonlocal mat
993 nonlocal info
994
995 # Find rootnodes
996 #
997 if node == None:
998 #{
999 _graph_read.extracted = []
1000
1001 for node_idname in node_def:
1002 #{
1003 for n in mat.node_tree.nodes:
1004 #{
1005 if n.name == node_idname:
1006 #{
1007 node_def = node_def[node_idname]
1008 node = n
1009 break
1010 #}
1011 #}
1012 #}
1013 #}
1014
1015 for link in node_def:
1016 #{
1017 link_def = node_def[link]
1018
1019 if isinstance( link_def, dict ):
1020 #{
1021 node_link = node.inputs[link]
1022
1023 if node_link.is_linked:
1024 #{
1025 # look for definitions for the connected node type
1026 #
1027 from_node = node_link.links[0].from_node
1028
1029 node_name = from_node.name.split('.')[0]
1030 if node_name in link_def:
1031 #{
1032 from_node_def = link_def[ node_name ]
1033
1034 _graph_read( from_node_def, from_node, depth+1 )
1035 #}
1036
1037 # No definition! :(
1038 # TODO: Make a warning for this?
1039 #}
1040 else:
1041 #{
1042 if "default" in link_def:
1043 #{
1044 prop = link_def['default']
1045 info[prop] = node_link.default_value
1046 #}
1047 #}
1048 #}
1049 else:
1050 #{
1051 prop = link_def
1052 info[prop] = getattr( node, link )
1053 #}
1054 #}
1055 #}
1056
1057 _graph_read( cxr_graph_mapping )
1058 return info
1059 #}
1060
1061 # Add a material to the material buffer. Returns 0 (None ID) if invalid
1062 #
1063 def encoder_process_material( mat ):
1064 #{
1065 global g_encoder
1066
1067 if mat == None:
1068 return 0
1069
1070 cache = g_encoder['material_cache']
1071 buffer = g_encoder['data']['material']
1072
1073 if mat.name in cache:
1074 return cache[mat.name]
1075
1076 cache[mat.name] = len( buffer )
1077
1078 dest = mdl_material()
1079 dest.pstr_name = encoder_process_pstr( mat.name )
1080
1081 flags = 0x00
1082 if mat.cv_data.skate_surface: flags |= 0x1
1083 if mat.cv_data.collision: flags |= 0x2
1084 if mat.cv_data.grow_grass: flags |= 0x4
1085 dest.flags = flags
1086
1087 if mat.cv_data.surface_prop == 'concrete': dest.surface_prop = 0
1088 if mat.cv_data.surface_prop == 'wood': dest.surface_prop = 1
1089 if mat.cv_data.surface_prop == 'grass': dest.surface_prop = 2
1090
1091 if mat.cv_data.shader == 'standard': dest.shader = 0
1092 if mat.cv_data.shader == 'standard_cutout': dest.shader = 1
1093 if mat.cv_data.shader == 'terrain_blend':
1094 #{
1095 dest.shader = 2
1096
1097 dest.colour[0] = pow( mat.cv_data.sand_colour[0], 1.0/2.2 )
1098 dest.colour[1] = pow( mat.cv_data.sand_colour[1], 1.0/2.2 )
1099 dest.colour[2] = pow( mat.cv_data.sand_colour[2], 1.0/2.2 )
1100 dest.colour[3] = 1.0
1101
1102 dest.colour1[0] = mat.cv_data.blend_offset[0]
1103 dest.colour1[1] = mat.cv_data.blend_offset[1]
1104 #}
1105
1106 if mat.cv_data.shader == 'vertex_blend':
1107 #{
1108 dest.shader = 3
1109
1110 dest.colour1[0] = mat.cv_data.blend_offset[0]
1111 dest.colour1[1] = mat.cv_data.blend_offset[1]
1112 #}
1113
1114 if mat.cv_data.shader == 'water':
1115 #{
1116 dest.shader = 4
1117
1118 dest.colour[0] = pow( mat.cv_data.shore_colour[0], 1.0/2.2 )
1119 dest.colour[1] = pow( mat.cv_data.shore_colour[1], 1.0/2.2 )
1120 dest.colour[2] = pow( mat.cv_data.shore_colour[2], 1.0/2.2 )
1121 dest.colour[3] = 1.0
1122 dest.colour1[0] = pow( mat.cv_data.ocean_colour[0], 1.0/2.2 )
1123 dest.colour1[1] = pow( mat.cv_data.ocean_colour[1], 1.0/2.2 )
1124 dest.colour1[2] = pow( mat.cv_data.ocean_colour[2], 1.0/2.2 )
1125 dest.colour1[3] = 1.0
1126 #}
1127
1128 inf = material_info( mat )
1129
1130 if mat.cv_data.shader == 'standard' or \
1131 mat.cv_data.shader == 'standard_cutout' or \
1132 mat.cv_data.shader == 'terrain_blend' or \
1133 mat.cv_data.shader == 'vertex_blend':
1134 #{
1135 if 'tex_diffuse' in inf:
1136 dest.tex_diffuse = encoder_process_texture(inf['tex_diffuse'])
1137 #}
1138
1139 buffer += [dest]
1140 return cache[mat.name]
1141 #}
1142
1143 # Create a tree structure containing all the objects in the collection
1144 #
1145 def encoder_build_scene_graph( collection ):
1146 #{
1147 global g_encoder
1148
1149 print( " creating scene graph" )
1150
1151 # initialize root
1152 #
1153 graph = g_encoder['scene_graph']
1154 graph_lookup = g_encoder['graph_lookup']
1155 graph["obj"] = None
1156 graph["depth"] = 0
1157 graph["children"] = []
1158 graph["uid"] = 0
1159 graph["parent"] = None
1160
1161 def _new_uid():
1162 #{
1163 global g_encoder
1164 uid = g_encoder['uid_count']
1165 g_encoder['uid_count'] += 1
1166 return uid
1167 #}
1168
1169 for obj in collection.all_objects:
1170 #{
1171 if obj.parent: continue
1172
1173 def _extend( p, n, d ):
1174 #{
1175 uid = _new_uid()
1176 tree = {}
1177 tree["uid"] = uid
1178 tree["children"] = []
1179 tree["depth"] = d
1180 tree["obj"] = n
1181 tree["parent"] = p
1182 n.cv_data.uid = uid
1183
1184 # Descend into amature
1185 #
1186 if n.type == 'ARMATURE':
1187 #{
1188 tree["bones"] = [None] # None is the root transform
1189 tree["ik_count"] = 0
1190 tree["collider_count"] = 0
1191
1192 # Here also collects some information about constraints, ik and
1193 # counts colliders for the armature.
1194 #
1195 def _extendb( p, n, d ):
1196 #{
1197 nonlocal tree
1198
1199 btree = {}
1200 btree["bone"] = n
1201 btree["linked_armature"] = tree
1202 btree["uid"] = _new_uid()
1203 btree["children"] = []
1204 btree["depth"] = d
1205 btree["parent"] = p
1206 tree["bones"] += [n.name]
1207
1208 for c in n.children:
1209 #{
1210 _extendb( btree, c, d+1 )
1211 #}
1212
1213 for c in tree['obj'].pose.bones[n.name].constraints:
1214 #{
1215 if c.type == 'IK':
1216 #{
1217 btree["ik_target"] = c.subtarget
1218 btree["ik_pole"] = c.pole_subtarget
1219 tree["ik_count"] += 1
1220 #}
1221 #}
1222
1223 if n.cv_data.collider:
1224 tree['collider_count'] += 1
1225
1226 btree['deform'] = n.use_deform
1227 p['children'] += [btree]
1228 #}
1229
1230 for b in n.data.bones:
1231 if not b.parent:
1232 _extendb( tree, b, d+1 )
1233 #}
1234
1235 # Recurse into children of this object
1236 #
1237 for obj1 in n.children:
1238 #{
1239 nonlocal collection
1240 for c1 in obj1.users_collection:
1241 #{
1242 if c1 == collection:
1243 #{
1244 _extend( tree, obj1, d+1 )
1245 break
1246 #}
1247 #}
1248 #}
1249
1250 p["children"] += [tree]
1251 graph_lookup[n] = tree
1252
1253 #}
1254
1255 _extend( graph, obj, 1 )
1256
1257 #}
1258 #}
1259
1260
1261 # Kind of a useless thing i made but it looks cool and adds complexity!!1
1262 #
1263 def encoder_graph_iterator( root ):
1264 #{
1265 for c in root['children']:
1266 #{
1267 yield c
1268 yield from encoder_graph_iterator(c)
1269 #}
1270 #}
1271
1272
1273 # Push a vertex into the model file, or return a cached index (c_uint32)
1274 #
1275 def encoder_vertex_push( vertex_reference, co,norm,uv,colour,groups,weights ):
1276 #{
1277 global g_encoder
1278 buffer = g_encoder['data']['vertex']
1279
1280 TOLERENCE = 4
1281 m = float(10**TOLERENCE)
1282
1283 # Would be nice to know if this can be done faster than it currently runs,
1284 # its quite slow.
1285 #
1286 key = (int(co[0]*m+0.5),
1287 int(co[1]*m+0.5),
1288 int(co[2]*m+0.5),
1289 int(norm[0]*m+0.5),
1290 int(norm[1]*m+0.5),
1291 int(norm[2]*m+0.5),
1292 int(uv[0]*m+0.5),
1293 int(uv[1]*m+0.5),
1294 colour[0]*m+0.5, # these guys are already quantized
1295 colour[1]*m+0.5, # .
1296 colour[2]*m+0.5, # .
1297 colour[3]*m+0.5, # .
1298 weights[0]*m+0.5, # v
1299 weights[1]*m+0.5,
1300 weights[2]*m+0.5,
1301 weights[3]*m+0.5,
1302 groups[0]*m+0.5,
1303 groups[1]*m+0.5,
1304 groups[2]*m+0.5,
1305 groups[3]*m+0.5)
1306
1307 if key in vertex_reference:
1308 return vertex_reference[key]
1309 else:
1310 #{
1311 index = c_uint32( len(vertex_reference) )
1312 vertex_reference[key] = index
1313
1314 v = mdl_vert()
1315 v.co[0] = co[0]
1316 v.co[1] = co[2]
1317 v.co[2] = -co[1]
1318 v.norm[0] = norm[0]
1319 v.norm[1] = norm[2]
1320 v.norm[2] = -norm[1]
1321 v.uv[0] = uv[0]
1322 v.uv[1] = uv[1]
1323 v.colour[0] = colour[0]
1324 v.colour[1] = colour[1]
1325 v.colour[2] = colour[2]
1326 v.colour[3] = colour[3]
1327 v.weights[0] = weights[0]
1328 v.weights[1] = weights[1]
1329 v.weights[2] = weights[2]
1330 v.weights[3] = weights[3]
1331 v.groups[0] = groups[0]
1332 v.groups[1] = groups[1]
1333 v.groups[2] = groups[2]
1334 v.groups[3] = groups[3]
1335
1336 buffer += [v]
1337 return index
1338 #}
1339 #}
1340
1341
1342 # Compile a mesh (or use one from the cache) onto node, based on node_def
1343 # No return value
1344 #
1345 def encoder_compile_mesh( node, node_def ):
1346 #{
1347 global g_encoder
1348
1349 graph = g_encoder['scene_graph']
1350 graph_lookup = g_encoder['graph_lookup']
1351 mesh_cache = g_encoder['mesh_cache']
1352 obj = node_def['obj']
1353 armature_def = None
1354 can_use_cache = True
1355
1356 # Check for modifiers that typically change the data per-instance
1357 # there is no well defined rule for the choices here, its just what i've
1358 # needed while producing the game.
1359 #
1360 # It may be possible to detect these cases automatically.
1361 #
1362 for mod in obj.modifiers:
1363 #{
1364 if mod.type == 'DATA_TRANSFER' or mod.type == 'SHRINKWRAP' or \
1365 mod.type == 'BOOLEAN' or mod.type == 'CURVE' or \
1366 mod.type == 'ARRAY':
1367 #{
1368 can_use_cache = False
1369 #}
1370
1371 if mod.type == 'ARMATURE':
1372 armature_def = graph_lookup[mod.object]
1373
1374 # Check the cache first
1375 #
1376 if can_use_cache and (obj.data.name in mesh_cache):
1377 #{
1378 ref = mesh_cache[obj.data.name]
1379 node.submesh_start = ref.submesh_start
1380 node.submesh_count = ref.submesh_count
1381 return
1382 #}
1383
1384 # Compile a whole new mesh
1385 #
1386 node.submesh_start = len( g_encoder['data']['submesh'] )
1387 node.submesh_count = 0
1388
1389 dgraph = bpy.context.evaluated_depsgraph_get()
1390 data = obj.evaluated_get(dgraph).data
1391 data.calc_loop_triangles()
1392 data.calc_normals_split()
1393
1394 # Mesh is split into submeshes based on their material
1395 #
1396 mat_list = data.materials if len(data.materials) > 0 else [None]
1397 for material_id, mat in enumerate(mat_list):
1398 #{
1399 mref = {}
1400
1401 sm = mdl_submesh()
1402 sm.indice_start = len( g_encoder['data']['indice'] )
1403 sm.vertex_start = len( g_encoder['data']['vertex'] )
1404 sm.vertex_count = 0
1405 sm.indice_count = 0
1406 sm.material_id = encoder_process_material( mat )
1407
1408 for i in range(3):
1409 #{
1410 sm.bbx[0][i] = 999999
1411 sm.bbx[1][i] = -999999
1412 #}
1413
1414 # Keep a reference to very very very similar vertices
1415 #
1416 vertex_reference = {}
1417
1418 # Write the vertex / indice data
1419 #
1420 for tri_index, tri in enumerate(data.loop_triangles):
1421 #{
1422 if tri.material_index != material_id:
1423 continue
1424
1425 for j in range(3):
1426 #{
1427 vert = data.vertices[tri.vertices[j]]
1428 li = tri.loops[j]
1429 vi = data.loops[li].vertex_index
1430
1431 # Gather vertex information
1432 #
1433 co = vert.co
1434 norm = data.loops[li].normal
1435 uv = (0,0)
1436 colour = (255,255,255,255)
1437 groups = [0,0,0,0]
1438 weights = [0,0,0,0]
1439
1440 # Uvs
1441 #
1442 if data.uv_layers:
1443 uv = data.uv_layers.active.data[li].uv
1444
1445 # Vertex Colours
1446 #
1447 if data.vertex_colors:
1448 #{
1449 colour = data.vertex_colors.active.data[li].color
1450 colour = (int(colour[0]*255.0),\
1451 int(colour[1]*255.0),\
1452 int(colour[2]*255.0),\
1453 int(colour[3]*255.0))
1454 #}
1455
1456 # Weight groups: truncates to the 3 with the most influence. The
1457 # fourth bone ID is never used by the shader so it is
1458 # always 0
1459 #
1460 if armature_def:
1461 #{
1462 src_groups = [_ for _ in data.vertices[vi].groups \
1463 if obj.vertex_groups[_.group].name in \
1464 armature_def['bones']]
1465
1466 weight_groups = sorted( src_groups, key = \
1467 lambda a: a.weight, reverse=True )
1468 tot = 0.0
1469 for ml in range(3):
1470 #{
1471 if len(weight_groups) > ml:
1472 #{
1473 g = weight_groups[ml]
1474 name = obj.vertex_groups[g.group].name
1475 weight = g.weight
1476
1477 weights[ml] = weight
1478 groups[ml] = armature_def['bones'].index(name)
1479 tot += weight
1480 #}
1481 #}
1482
1483 if len(weight_groups) > 0:
1484 #{
1485 inv_norm = (1.0/tot) * 65535.0
1486 for ml in range(3):
1487 #{
1488 weights[ml] = int( weights[ml] * inv_norm )
1489 weights[ml] = min( weights[ml], 65535 )
1490 weights[ml] = max( weights[ml], 0 )
1491 #}
1492 #}
1493
1494 # Add vertex and expand bound box
1495 #
1496 index = encoder_vertex_push( vertex_reference, co, \
1497 norm, \
1498 uv, \
1499 colour, \
1500 groups, \
1501 weights )
1502 g_encoder['data']['indice'] += [index]
1503 #}
1504 #}
1505
1506 # How many unique verts did we add in total
1507 #
1508 sm.vertex_count = len(g_encoder['data']['vertex']) - sm.vertex_start
1509 sm.indice_count = len(g_encoder['data']['indice']) - sm.indice_start
1510
1511 # Make sure bounding box isn't -inf -> inf if no vertices
1512 #
1513 if sm.vertex_count == 0:
1514 for j in range(2):
1515 for i in range(3):
1516 sm.bbx[j][i] = 0
1517 else:
1518 #{
1519 for j in range(sm.vertex_count):
1520 #{
1521 vert = g_encoder['data']['vertex'][ sm.vertex_start + j ]
1522
1523 for i in range(3):
1524 #{
1525 sm.bbx[0][i] = min( sm.bbx[0][i], vert.co[i] )
1526 sm.bbx[1][i] = max( sm.bbx[1][i], vert.co[i] )
1527 #}
1528 #}
1529 #}
1530
1531 # Add submesh to encoder
1532 #
1533 g_encoder['data']['submesh'] += [sm]
1534 node.submesh_count += 1
1535
1536 #}
1537
1538 # Save a reference to this node since we want to reuse the submesh indices
1539 # later.
1540 g_encoder['mesh_cache'][obj.data.name] = node
1541 #}
1542
1543
1544 def encoder_compile_ent_as( name, node, node_def ):
1545 #{
1546 global g_encoder
1547
1548 if name == 'classtype_none':
1549 #{
1550 node.offset = 0
1551 node.classtype = 0
1552 return
1553 #}
1554 elif name not in globals():
1555 #{
1556 print( "Classtype '" +name + "' is unknown!" )
1557 return
1558 #}
1559
1560 buffer = g_encoder['data']['entdata']
1561 node.offset = len(buffer)
1562
1563 cl = globals()[ name ]
1564 inst = cl()
1565 inst.encode_obj( node, node_def )
1566
1567 buffer.extend( bytearray(inst) )
1568 bytearray_align_to( buffer, 4 )
1569 #}
1570
1571 # Compiles animation data into model and gives us some extra node_def entries
1572 #
1573 def encoder_compile_armature( node, node_def ):
1574 #{
1575 global g_encoder
1576
1577 entdata = g_encoder['data']['entdata']
1578 animdata = g_encoder['data']['anim']
1579 keyframedata = g_encoder['data']['keyframe']
1580 mesh_cache = g_encoder['mesh_cache']
1581 obj = node_def['obj']
1582 bones = node_def['bones']
1583
1584 # extra info
1585 node_def['anim_start'] = len(animdata)
1586 node_def['anim_count'] = 0
1587
1588 # Compile anims
1589 #
1590 if obj.animation_data:
1591 #{
1592 # So we can restore later
1593 #
1594 previous_frame = bpy.context.scene.frame_current
1595 previous_action = obj.animation_data.action
1596 POSE_OR_REST_CACHE = obj.data.pose_position
1597 obj.data.pose_position = 'POSE'
1598
1599 for NLALayer in obj.animation_data.nla_tracks:
1600 #{
1601 for NLAStrip in NLALayer.strips:
1602 #{
1603 # set active
1604 #
1605 for a in bpy.data.actions:
1606 #{
1607 if a.name == NLAStrip.name:
1608 #{
1609 obj.animation_data.action = a
1610 break
1611 #}
1612 #}
1613
1614 # Clip to NLA settings
1615 #
1616 anim_start = int(NLAStrip.action_frame_start)
1617 anim_end = int(NLAStrip.action_frame_end)
1618
1619 # Export strips
1620 #
1621 anim = mdl_animation()
1622 anim.pstr_name = encoder_process_pstr( NLAStrip.action.name )
1623 anim.rate = 30.0
1624 anim.offset = len(keyframedata)
1625 anim.length = anim_end-anim_start
1626
1627 # Export the keyframes
1628 for frame in range(anim_start,anim_end):
1629 #{
1630 bpy.context.scene.frame_set(frame)
1631
1632 for bone_name in bones:
1633 #{
1634 for pb in obj.pose.bones:
1635 #{
1636 if pb.name != bone_name: continue
1637
1638 rb = obj.data.bones[ bone_name ]
1639
1640 # relative bone matrix
1641 if rb.parent is not None:
1642 #{
1643 offset_mtx = rb.parent.matrix_local
1644 offset_mtx = offset_mtx.inverted_safe() @ \
1645 rb.matrix_local
1646
1647 inv_parent = pb.parent.matrix @ offset_mtx
1648 inv_parent.invert_safe()
1649 fpm = inv_parent @ pb.matrix
1650 #}
1651 else:
1652 #{
1653 bone_mtx = rb.matrix.to_4x4()
1654 local_inv = rb.matrix_local.inverted_safe()
1655 fpm = bone_mtx @ local_inv @ pb.matrix
1656 #}
1657
1658 loc, rot, sca = fpm.decompose()
1659
1660 # local position
1661 final_pos = Vector(( loc[0], loc[2], -loc[1] ))
1662
1663 # rotation
1664 lc_m = pb.matrix_channel.to_3x3()
1665 if pb.parent is not None:
1666 #{
1667 smtx = pb.parent.matrix_channel.to_3x3()
1668 lc_m = smtx.inverted() @ lc_m
1669 #}
1670 rq = lc_m.to_quaternion()
1671
1672 kf = mdl_keyframe()
1673 kf.co[0] = final_pos[0]
1674 kf.co[1] = final_pos[1]
1675 kf.co[2] = final_pos[2]
1676
1677 kf.q[0] = rq[1]
1678 kf.q[1] = rq[3]
1679 kf.q[2] = -rq[2]
1680 kf.q[3] = rq[0]
1681
1682 # scale
1683 kf.s[0] = sca[0]
1684 kf.s[1] = sca[2]
1685 kf.s[2] = sca[1]
1686
1687 keyframedata += [kf]
1688 break
1689 #}
1690 #}
1691 #}
1692
1693 # Add to animation buffer
1694 #
1695 animdata += [anim]
1696 node_def['anim_count'] += 1
1697
1698 # Report progress
1699 #
1700 status_name = F" " + " |"*(node_def['depth']-1)
1701 print( F"{status_name} | *anim: {NLAStrip.action.name}" )
1702 #}
1703 #}
1704
1705 # Restore context to how it was before
1706 #
1707 bpy.context.scene.frame_set( previous_frame )
1708 obj.animation_data.action = previous_action
1709 obj.data.pose_position = POSE_OR_REST_CACHE
1710 #}
1711 #}
1712
1713 # We are trying to compile this node_def
1714 #
1715 def encoder_process_definition( node_def ):
1716 #{
1717 global g_encoder
1718
1719 # data sources for object/bone are taken differently
1720 #
1721 if 'obj' in node_def:
1722 #{
1723 obj = node_def['obj']
1724 obj_type = obj.type
1725 obj_co = obj.location
1726
1727 if obj_type == 'ARMATURE':
1728 obj_classtype = 'classtype_skeleton'
1729 else:
1730 #{
1731 obj_classtype = obj.cv_data.classtype
1732
1733 # Check for armature deform
1734 #
1735 for mod in obj.modifiers:
1736 #{
1737 if mod.type == 'ARMATURE':
1738 #{
1739 obj_classtype = 'classtype_skin'
1740
1741 # Make sure to freeze armature in rest while we collect
1742 # vertex information
1743 #
1744 armature_def = g_encoder['graph_lookup'][mod.object]
1745 POSE_OR_REST_CACHE = armature_def['obj'].data.pose_position
1746 armature_def['obj'].data.pose_position = 'REST'
1747 node_def['linked_armature'] = armature_def
1748 break
1749 #}
1750 #}
1751 #}
1752 #}
1753
1754 elif 'bone' in node_def:
1755 #{
1756 obj = node_def['bone']
1757 obj_type = 'BONE'
1758 obj_co = obj.head_local
1759 obj_classtype = 'classtype_bone'
1760 #}
1761
1762 # Create node
1763 #
1764 node = mdl_node()
1765 node.pstr_name = encoder_process_pstr( obj.name )
1766
1767 if node_def["parent"]:
1768 node.parent = node_def["parent"]["uid"]
1769
1770 # Setup transform
1771 #
1772 node.co[0] = obj_co[0]
1773 node.co[1] = obj_co[2]
1774 node.co[2] = -obj_co[1]
1775
1776 # Convert rotation quat to our space type
1777 #
1778 quat = obj.matrix_local.to_quaternion()
1779 node.q[0] = quat[1]
1780 node.q[1] = quat[3]
1781 node.q[2] = -quat[2]
1782 node.q[3] = quat[0]
1783
1784 # Bone scale is just a vector to the tail
1785 #
1786 if obj_type == 'BONE':
1787 #{
1788 node.s[0] = obj.tail_local[0] - node.co[0]
1789 node.s[1] = obj.tail_local[2] - node.co[1]
1790 node.s[2] = -obj.tail_local[1] - node.co[2]
1791 #}
1792 else:
1793 #{
1794 node.s[0] = obj.scale[0]
1795 node.s[1] = obj.scale[2]
1796 node.s[2] = obj.scale[1]
1797 #}
1798
1799 # Report status
1800 #
1801 tot_uid = g_encoder['uid_count']-1
1802 obj_uid = node_def['uid']
1803 obj_depth = node_def['depth']-1
1804
1805 status_id = F" [{obj_uid: 3}/{tot_uid}]" + " |"*obj_depth
1806 status_name = status_id + F" L {obj.name}"
1807
1808 if obj_classtype != 'classtype_none': status_type = obj_classtype
1809 else: status_type = obj_type
1810
1811 status_parent = F"{node.parent: 3}"
1812 status_armref = ""
1813
1814 if obj_classtype == 'classtype_skin':
1815 status_armref = F" [armature -> {armature_def['obj'].cv_data.uid}]"
1816
1817 print(F"{status_name:<32} {status_type:<22} {status_parent} {status_armref}")
1818
1819 # Process mesh if needed
1820 #
1821 if obj_type == 'MESH':
1822 #{
1823 encoder_compile_mesh( node, node_def )
1824 #}
1825 elif obj_type == 'ARMATURE':
1826 #{
1827 encoder_compile_armature( node, node_def )
1828 #}
1829
1830 encoder_compile_ent_as( obj_classtype, node, node_def )
1831
1832 # Make sure to reset the armature we just mucked about with
1833 #
1834 if obj_classtype == 'classtype_skin':
1835 armature_def['obj'].data.pose_position = POSE_OR_REST_CACHE
1836
1837 g_encoder['data']['node'] += [node]
1838 #}
1839
1840 # The post processing step or the pre processing to the writing step
1841 #
1842 def encoder_write_to_file( path ):
1843 #{
1844 global g_encoder
1845
1846 # Compile down to a byte array
1847 #
1848 header = g_encoder['header']
1849 file_pos = sizeof(header)
1850 file_data = bytearray()
1851 print( " Compositing data arrays" )
1852
1853 for array_name in g_encoder['data']:
1854 #{
1855 file_pos += bytearray_align_to( file_data, 16, sizeof(header) )
1856 arr = g_encoder['data'][array_name]
1857
1858 setattr( header, array_name + "_offset", file_pos )
1859
1860 print( F" {array_name:<16} @{file_pos:> 8X}[{len(arr)}]" )
1861
1862 if isinstance( arr, bytearray ):
1863 #{
1864 setattr( header, array_name + "_size", len(arr) )
1865
1866 file_data.extend( arr )
1867 file_pos += len(arr)
1868 #}
1869 else:
1870 #{
1871 setattr( header, array_name + "_count", len(arr) )
1872
1873 for item in arr:
1874 #{
1875 bbytes = bytearray(item)
1876 file_data.extend( bbytes )
1877 file_pos += sizeof(item)
1878 #}
1879 #}
1880 #}
1881
1882 # This imperitive for this field to be santized in the future!
1883 #
1884 header.file_length = file_pos
1885
1886 print( " Writing file" )
1887 # Write header and data chunk to file
1888 #
1889 fp = open( path, "wb" )
1890 fp.write( bytearray( header ) )
1891 fp.write( file_data )
1892 fp.close()
1893 #}
1894
1895 # Main compiler, uses string as the identifier for the collection
1896 #
1897 def write_model(collection_name):
1898 #{
1899 global g_encoder
1900 print( F"Model graph | Create mode '{collection_name}'" )
1901
1902 collection = bpy.data.collections[collection_name]
1903
1904 encoder_init( collection )
1905 encoder_build_scene_graph( collection )
1906
1907 # Compile
1908 #
1909 print( " Comping objects" )
1910 it = encoder_graph_iterator( g_encoder['scene_graph'] )
1911 for node_def in it:
1912 encoder_process_definition( node_def )
1913
1914 # Write
1915 #
1916 # TODO HOLY
1917 path = F"/home/harry/Documents/carve/models_src/{collection_name}.mdl"
1918 encoder_write_to_file( path )
1919
1920 print( F"Completed {collection_name}.mdl" )
1921 #}
1922
1923 # ---------------------------------------------------------------------------- #
1924 # #
1925 # GUI section #
1926 # #
1927 # ---------------------------------------------------------------------------- #
1928
1929 cv_view_draw_handler = None
1930 cv_view_shader = gpu.shader.from_builtin('3D_SMOOTH_COLOR')
1931 cv_view_verts = []
1932 cv_view_colours = []
1933 cv_view_course_i = 0
1934
1935 # Draw axis alligned sphere at position with radius
1936 #
1937 def cv_draw_sphere( pos, radius, colour ):
1938 #{
1939 global cv_view_verts, cv_view_colours
1940
1941 ly = pos + Vector((0,0,radius))
1942 lx = pos + Vector((0,radius,0))
1943 lz = pos + Vector((0,0,radius))
1944
1945 pi = 3.14159265358979323846264
1946
1947 for i in range(16):
1948 #{
1949 t = ((i+1.0) * 1.0/16.0) * pi * 2.0
1950 s = math.sin(t)
1951 c = math.cos(t)
1952
1953 py = pos + Vector((s*radius,0.0,c*radius))
1954 px = pos + Vector((s*radius,c*radius,0.0))
1955 pz = pos + Vector((0.0,s*radius,c*radius))
1956
1957 cv_view_verts += [ px, lx ]
1958 cv_view_verts += [ py, ly ]
1959 cv_view_verts += [ pz, lz ]
1960
1961 cv_view_colours += [ colour, colour, colour, colour, colour, colour ]
1962
1963 ly = py
1964 lx = px
1965 lz = pz
1966 #}
1967 cv_draw_lines()
1968 #}
1969
1970 # Draw transformed -1 -> 1 cube
1971 #
1972 def cv_draw_ucube( transform, colour ):
1973 #{
1974 global cv_view_verts, cv_view_colours
1975
1976 a = Vector((-1,-1,-1))
1977 b = Vector((1,1,1))
1978
1979 vs = [None]*8
1980 vs[0] = transform @ Vector((a[0], a[1], a[2]))
1981 vs[1] = transform @ Vector((a[0], b[1], a[2]))
1982 vs[2] = transform @ Vector((b[0], b[1], a[2]))
1983 vs[3] = transform @ Vector((b[0], a[1], a[2]))
1984 vs[4] = transform @ Vector((a[0], a[1], b[2]))
1985 vs[5] = transform @ Vector((a[0], b[1], b[2]))
1986 vs[6] = transform @ Vector((b[0], b[1], b[2]))
1987 vs[7] = transform @ Vector((b[0], a[1], b[2]))
1988
1989 indices = [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),\
1990 (0,4),(1,5),(2,6),(3,7)]
1991
1992 for l in indices:
1993 #{
1994 v0 = vs[l[0]]
1995 v1 = vs[l[1]]
1996 cv_view_verts += [(v0[0],v0[1],v0[2])]
1997 cv_view_verts += [(v1[0],v1[1],v1[2])]
1998 cv_view_colours += [(0,1,0,1),(0,1,0,1)]
1999 #}
2000 cv_draw_lines()
2001 #}
2002
2003 # Draw line with colour
2004 #
2005 def cv_draw_line( p0, p1, colour ):
2006 #{
2007 global cv_view_verts, cv_view_colours
2008
2009 cv_view_verts += [p0,p1]
2010 cv_view_colours += [colour, colour]
2011 cv_draw_lines()
2012 #}
2013
2014 # Draw line with colour(s)
2015 #
2016 def cv_draw_line2( p0, p1, c0, c1 ):
2017 #{
2018 global cv_view_verts, cv_view_colours
2019
2020 cv_view_verts += [p0,p1]
2021 cv_view_colours += [c0,c1]
2022 cv_draw_lines()
2023 #}
2024
2025 # Just the tx because we dont really need ty for this app
2026 #
2027 def cv_tangent_basis_tx( n, tx ):
2028 #{
2029 if abs( n[0] ) >= 0.57735027:
2030 #{
2031 tx[0] = n[1]
2032 tx[1] = -n[0]
2033 tx[2] = 0.0
2034 #}
2035 else:
2036 #{
2037 tx[0] = 0.0
2038 tx[1] = n[2]
2039 tx[2] = -n[1]
2040 #}
2041
2042 tx.normalize()
2043 #}
2044
2045 # Draw coloured arrow
2046 #
2047 def cv_draw_arrow( p0, p1, c0 ):
2048 #{
2049 global cv_view_verts, cv_view_colours
2050
2051 n = p1-p0
2052 midpt = p0 + n*0.5
2053 n.normalize()
2054
2055 tx = Vector((1,0,0))
2056 cv_tangent_basis_tx( n, tx )
2057
2058 cv_view_verts += [p0,p1, midpt+(tx-n)*0.15,midpt, midpt+(-tx-n)*0.15,midpt ]
2059 cv_view_colours += [c0,c0,c0,c0,c0,c0]
2060 cv_draw_lines()
2061 #}
2062
2063 # Drawhandles of a bezier control point
2064 #
2065 def cv_draw_bhandle( obj, direction, colour ):
2066 #{
2067 global cv_view_verts, cv_view_colours
2068
2069 p0 = obj.location
2070 h0 = obj.matrix_world @ Vector((0,direction,0))
2071
2072 cv_view_verts += [p0]
2073 cv_view_verts += [h0]
2074 cv_view_colours += [colour,colour]
2075 cv_draw_lines()
2076 #}
2077
2078 # Draw a bezier curve (at fixed resolution 10)
2079 #
2080 def cv_draw_bezier( p0,h0,p1,h1,c0,c1 ):
2081 #{
2082 global cv_view_verts, cv_view_colours
2083
2084 last = p0
2085 for i in range(10):
2086 #{
2087 t = (i+1)/10
2088 a0 = 1-t
2089
2090 tt = t*t
2091 ttt = tt*t
2092 p=ttt*p1+(3*tt-3*ttt)*h1+(3*ttt-6*tt+3*t)*h0+(3*tt-ttt-3*t+1)*p0
2093
2094 cv_view_verts += [(last[0],last[1],last[2])]
2095 cv_view_verts += [(p[0],p[1],p[2])]
2096 cv_view_colours += [c0*a0+c1*(1-a0),c0*a0+c1*(1-a0)]
2097
2098 last = p
2099 #}
2100 cv_draw_lines()
2101 #}
2102
2103 # I think this one extends the handles of the bezier otwards......
2104 #
2105 def cv_draw_sbpath( o0,o1,c0,c1,s0,s1 ):
2106 #{
2107 global cv_view_course_i
2108
2109 offs = ((cv_view_course_i % 2)*2-1) * cv_view_course_i * 0.02
2110
2111 p0 = o0.matrix_world @ Vector((offs, 0,0))
2112 h0 = o0.matrix_world @ Vector((offs, s0,0))
2113 p1 = o1.matrix_world @ Vector((offs, 0,0))
2114 h1 = o1.matrix_world @ Vector((offs,-s1,0))
2115
2116 cv_draw_bezier( p0,h0,p1,h1,c0,c1 )
2117 cv_draw_lines()
2118 #}
2119
2120 # Flush the lines buffers. This is called often because god help you if you want
2121 # to do fixed, fast buffers in this catastrophic programming language.
2122 #
2123 def cv_draw_lines():
2124 #{
2125 global cv_view_shader, cv_view_verts, cv_view_colours
2126
2127 if len(cv_view_verts) < 2:
2128 return
2129
2130 lines = batch_for_shader(\
2131 cv_view_shader, 'LINES', \
2132 { "pos":cv_view_verts, "color":cv_view_colours })
2133
2134 lines.draw( cv_view_shader )
2135
2136 cv_view_verts = []
2137 cv_view_colours = []
2138 #}
2139
2140 # I dont remember what this does exactly
2141 #
2142 def cv_draw_bpath( o0,o1,c0,c1 ):
2143 #{
2144 cv_draw_sbpath( o0,o1,c0,c1,1.0,1.0 )
2145 #}
2146
2147 # Semi circle to show the limit. and some lines
2148 #
2149 def draw_limit( obj, center, major, minor, amin, amax, colour ):
2150 #{
2151 global cv_view_verts, cv_view_colours
2152 f = 0.05
2153 ay = major*f
2154 ax = minor*f
2155
2156 for x in range(16):
2157 #{
2158 t0 = x/16
2159 t1 = (x+1)/16
2160 a0 = amin*(1.0-t0)+amax*t0
2161 a1 = amin*(1.0-t1)+amax*t1
2162
2163 p0 = center + major*f*math.cos(a0) + minor*f*math.sin(a0)
2164 p1 = center + major*f*math.cos(a1) + minor*f*math.sin(a1)
2165
2166 p0=obj.matrix_world @ p0
2167 p1=obj.matrix_world @ p1
2168 cv_view_verts += [p0,p1]
2169 cv_view_colours += [colour,colour]
2170
2171 if x == 0:
2172 #{
2173 cv_view_verts += [p0,center]
2174 cv_view_colours += [colour,colour]
2175 #}
2176 if x == 15:
2177 #{
2178 cv_view_verts += [p1,center]
2179 cv_view_colours += [colour,colour]
2180 #}
2181 #}
2182
2183 cv_view_verts += [center+major*1.2*f,center+major*f*0.8]
2184 cv_view_colours += [colour,colour]
2185
2186 cv_draw_lines()
2187 #}
2188
2189 # Draws constraints and stuff for the skeleton. This isnt documented and wont be
2190 #
2191 def draw_skeleton_helpers( obj ):
2192 #{
2193 global cv_view_verts, cv_view_colours
2194
2195 for bone in obj.data.bones:
2196 #{
2197 if bone.cv_data.collider and (obj.data.pose_position == 'REST'):
2198 #{
2199 c = bone.head_local
2200 a = bone.cv_data.v0
2201 b = bone.cv_data.v1
2202
2203 vs = [None]*8
2204 vs[0]=obj.matrix_world@Vector((c[0]+a[0],c[1]+a[1],c[2]+a[2]))
2205 vs[1]=obj.matrix_world@Vector((c[0]+a[0],c[1]+b[1],c[2]+a[2]))
2206 vs[2]=obj.matrix_world@Vector((c[0]+b[0],c[1]+b[1],c[2]+a[2]))
2207 vs[3]=obj.matrix_world@Vector((c[0]+b[0],c[1]+a[1],c[2]+a[2]))
2208 vs[4]=obj.matrix_world@Vector((c[0]+a[0],c[1]+a[1],c[2]+b[2]))
2209 vs[5]=obj.matrix_world@Vector((c[0]+a[0],c[1]+b[1],c[2]+b[2]))
2210 vs[6]=obj.matrix_world@Vector((c[0]+b[0],c[1]+b[1],c[2]+b[2]))
2211 vs[7]=obj.matrix_world@Vector((c[0]+b[0],c[1]+a[1],c[2]+b[2]))
2212
2213 indices = [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),\
2214 (0,4),(1,5),(2,6),(3,7)]
2215
2216 for l in indices:
2217 #{
2218 v0 = vs[l[0]]
2219 v1 = vs[l[1]]
2220
2221 cv_view_verts += [(v0[0],v0[1],v0[2])]
2222 cv_view_verts += [(v1[0],v1[1],v1[2])]
2223 cv_view_colours += [(0.5,0.5,0.5,0.5),(0.5,0.5,0.5,0.5)]
2224 #}
2225
2226 center = obj.matrix_world @ c
2227 if bone.cv_data.con0:
2228 #{
2229 draw_limit( obj, c, Vector((0,1,0)),Vector((0,0,1)), \
2230 bone.cv_data.mins[0], bone.cv_data.maxs[0], \
2231 (1,0,0,1))
2232 draw_limit( obj, c, Vector((0,0,1)),Vector((1,0,0)), \
2233 bone.cv_data.mins[1], bone.cv_data.maxs[1], \
2234 (0,1,0,1))
2235 draw_limit( obj, c, Vector((1,0,0)),Vector((0,1,0)), \
2236 bone.cv_data.mins[2], bone.cv_data.maxs[2], \
2237 (0,0,1,1))
2238 #}
2239 #}
2240 #}
2241 #}
2242
2243 def cv_draw():
2244 #{
2245 global cv_view_shader
2246 global cv_view_verts
2247 global cv_view_colours
2248 global cv_view_course_i
2249
2250 cv_view_course_i = 0
2251 cv_view_verts = []
2252 cv_view_colours = []
2253
2254 cv_view_shader.bind()
2255 gpu.state.depth_mask_set(False)
2256 gpu.state.line_width_set(2.0)
2257 gpu.state.face_culling_set('BACK')
2258 gpu.state.depth_test_set('LESS')
2259 gpu.state.blend_set('NONE')
2260
2261 for obj in bpy.context.collection.objects:
2262 #{
2263 if obj.type == 'ARMATURE':
2264 #{
2265 if obj.data.pose_position == 'REST':
2266 draw_skeleton_helpers( obj )
2267 #}
2268 else:
2269 #{
2270 classtype = obj.cv_data.classtype
2271 if (classtype != 'classtype_none') and (classtype in globals()):
2272 #{
2273 cl = globals()[ classtype ]
2274
2275 if getattr( cl, "draw_scene_helpers", None ):
2276 #{
2277 cl.draw_scene_helpers( obj )
2278 #}
2279 #}
2280 #}
2281 #}
2282
2283 cv_draw_lines()
2284 return
2285 #}
2286
2287
2288 # ---------------------------------------------------------------------------- #
2289 # #
2290 # Blender #
2291 # #
2292 # ---------------------------------------------------------------------------- #
2293
2294 # Checks whether this object has a classtype assigned. we can only target other
2295 # classes
2296 def cv_poll_target(scene, obj):
2297 #{
2298 if obj == bpy.context.active_object:
2299 return False
2300 if obj.cv_data.classtype == 'classtype_none':
2301 return False
2302
2303 return True
2304 #}
2305
2306 class CV_MESH_SETTINGS(bpy.types.PropertyGroup):
2307 #{
2308 v0: bpy.props.FloatVectorProperty(name="v0",size=3)
2309 v1: bpy.props.FloatVectorProperty(name="v1",size=3)
2310 v2: bpy.props.FloatVectorProperty(name="v2",size=3)
2311 v3: bpy.props.FloatVectorProperty(name="v3",size=3)
2312 #}
2313
2314 class CV_OBJ_SETTINGS(bpy.types.PropertyGroup):
2315 #{
2316 uid: bpy.props.IntProperty( name="" )
2317
2318 strp: bpy.props.StringProperty( name="strp" )
2319 intp: bpy.props.IntProperty( name="intp" )
2320 fltp: bpy.props.FloatProperty( name="fltp" )
2321 bp0: bpy.props.BoolProperty( name="bp0" )
2322 bp1: bpy.props.BoolProperty( name="bp1" )
2323 bp2: bpy.props.BoolProperty( name="bp2" )
2324 bp3: bpy.props.BoolProperty( name="bp3" )
2325
2326 target: bpy.props.PointerProperty( type=bpy.types.Object, name="target", \
2327 poll=cv_poll_target )
2328 target1: bpy.props.PointerProperty( type=bpy.types.Object, name="target1", \
2329 poll=cv_poll_target )
2330 target2: bpy.props.PointerProperty( type=bpy.types.Object, name="target2", \
2331 poll=cv_poll_target )
2332 target3: bpy.props.PointerProperty( type=bpy.types.Object, name="target3", \
2333 poll=cv_poll_target )
2334
2335 colour: bpy.props.FloatVectorProperty( name="colour",subtype='COLOR',\
2336 min=0.0,max=1.0)
2337
2338 classtype: bpy.props.EnumProperty(
2339 name="Format",
2340 items = [
2341 ('classtype_none', "classtype_none", "", 0),
2342 ('classtype_gate', "classtype_gate", "", 1),
2343 ('classtype_spawn', "classtype_spawn", "", 3),
2344 ('classtype_water', "classtype_water", "", 4),
2345 ('classtype_route_node', "classtype_route_node", "", 8 ),
2346 ('classtype_route', "classtype_route", "", 9 ),
2347 ('classtype_audio',"classtype_audio","",14),
2348 ('classtype_trigger',"classtype_trigger","",100),
2349 ('classtype_logic_achievement',"classtype_logic_achievement","",101),
2350 ('classtype_logic_relay',"classtype_logic_relay","",102),
2351 ])
2352 #}
2353
2354 class CV_BONE_SETTINGS(bpy.types.PropertyGroup):
2355 #{
2356 collider: bpy.props.BoolProperty(name="Collider",default=False)
2357 v0: bpy.props.FloatVectorProperty(name="v0",size=3)
2358 v1: bpy.props.FloatVectorProperty(name="v1",size=3)
2359
2360 con0: bpy.props.BoolProperty(name="Constriant 0",default=False)
2361 mins: bpy.props.FloatVectorProperty(name="mins",size=3)
2362 maxs: bpy.props.FloatVectorProperty(name="maxs",size=3)
2363 #}
2364
2365 class CV_BONE_PANEL(bpy.types.Panel):
2366 #{
2367 bl_label="Bone Config"
2368 bl_idname="SCENE_PT_cv_bone"
2369 bl_space_type='PROPERTIES'
2370 bl_region_type='WINDOW'
2371 bl_context='bone'
2372
2373 def draw(_,context):
2374 #{
2375 active_object = context.active_object
2376 if active_object == None: return
2377
2378 bone = active_object.data.bones.active
2379 if bone == None: return
2380
2381 _.layout.prop( bone.cv_data, "collider" )
2382 _.layout.prop( bone.cv_data, "v0" )
2383 _.layout.prop( bone.cv_data, "v1" )
2384
2385 _.layout.label( text="Angle Limits" )
2386 _.layout.prop( bone.cv_data, "con0" )
2387 _.layout.prop( bone.cv_data, "mins" )
2388 _.layout.prop( bone.cv_data, "maxs" )
2389 #}
2390 #}
2391
2392 class CV_SCENE_SETTINGS(bpy.types.PropertyGroup):
2393 #{
2394 use_hidden: bpy.props.BoolProperty( name="use hidden", default=False )
2395 export_dir: bpy.props.StringProperty( name="Export Dir", subtype='DIR_PATH' )
2396 #}
2397
2398 class CV_COLLECTION_SETTINGS(bpy.types.PropertyGroup):
2399 #{
2400 pack_textures: bpy.props.BoolProperty( name="Pack Textures", default=False )
2401 #}
2402
2403 class CV_MATERIAL_SETTINGS(bpy.types.PropertyGroup):
2404 #{
2405 shader: bpy.props.EnumProperty(
2406 name="Format",
2407 items = [
2408 ('standard',"standard","",0),
2409 ('standard_cutout', "standard_cutout", "", 1),
2410 ('terrain_blend', "terrain_blend", "", 2),
2411 ('vertex_blend', "vertex_blend", "", 3),
2412 ('water',"water","",4),
2413 ])
2414
2415 surface_prop: bpy.props.EnumProperty(
2416 name="Surface Property",
2417 items = [
2418 ('concrete','concrete','',0),
2419 ('wood','wood','',1),
2420 ('grass','grass','',2)
2421 ])
2422
2423 collision: bpy.props.BoolProperty( \
2424 name="Collisions Enabled",\
2425 default=True,\
2426 description = "Can the player collide with this material"\
2427 )
2428 skate_surface: bpy.props.BoolProperty( \
2429 name="Skate Surface", \
2430 default=True,\
2431 description = "Should the game try to target this surface?" \
2432 )
2433 grow_grass: bpy.props.BoolProperty( \
2434 name="Grow Grass", \
2435 default=False,\
2436 description = "Spawn grass sprites on this surface?" \
2437 )
2438 blend_offset: bpy.props.FloatVectorProperty( \
2439 name="Blend Offset", \
2440 size=2, \
2441 default=Vector((0.5,0.0)),\
2442 description="When surface is more than 45 degrees, add this vector " +\
2443 "to the UVs" \
2444 )
2445 sand_colour: bpy.props.FloatVectorProperty( \
2446 name="Sand Colour",\
2447 subtype='COLOR',\
2448 min=0.0,max=1.0,\
2449 default=Vector((0.79,0.63,0.48)),\
2450 description="Blend to this colour near the 0 coordinate on UP axis"\
2451 )
2452 shore_colour: bpy.props.FloatVectorProperty( \
2453 name="Shore Colour",\
2454 subtype='COLOR',\
2455 min=0.0,max=1.0,\
2456 default=Vector((0.03,0.32,0.61)),\
2457 description="Water colour at the shoreline"\
2458 )
2459 ocean_colour: bpy.props.FloatVectorProperty( \
2460 name="Ocean Colour",\
2461 subtype='COLOR',\
2462 min=0.0,max=1.0,\
2463 default=Vector((0.0,0.006,0.03)),\
2464 description="Water colour in the deep bits"\
2465 )
2466 #}
2467
2468 class CV_MATERIAL_PANEL(bpy.types.Panel):
2469 #{
2470 bl_label="Skate Rift material"
2471 bl_idname="MATERIAL_PT_cv_material"
2472 bl_space_type='PROPERTIES'
2473 bl_region_type='WINDOW'
2474 bl_context="material"
2475
2476 def draw(_,context):
2477 #{
2478 active_object = bpy.context.active_object
2479 if active_object == None: return
2480 active_mat = active_object.active_material
2481 if active_mat == None: return
2482
2483 info = material_info( active_mat )
2484
2485 _.layout.prop( active_mat.cv_data, "shader" )
2486 _.layout.prop( active_mat.cv_data, "surface_prop" )
2487 _.layout.prop( active_mat.cv_data, "collision" )
2488
2489 if active_mat.cv_data.collision:
2490 _.layout.prop( active_mat.cv_data, "skate_surface" )
2491 _.layout.prop( active_mat.cv_data, "grow_grass" )
2492
2493 if active_mat.cv_data.shader == "terrain_blend":
2494 #{
2495 box = _.layout.box()
2496 box.prop( active_mat.cv_data, "blend_offset" )
2497 box.prop( active_mat.cv_data, "sand_colour" )
2498 #}
2499 elif active_mat.cv_data.shader == "vertex_blend":
2500 #{
2501 box = _.layout.box()
2502 box.label( icon='INFO', text="Uses vertex colours, the R channel" )
2503 box.prop( active_mat.cv_data, "blend_offset" )
2504 #}
2505 elif active_mat.cv_data.shader == "water":
2506 #{
2507 box = _.layout.box()
2508 box.label( icon='INFO', text="Depth scale of 16 meters" )
2509 box.prop( active_mat.cv_data, "shore_colour" )
2510 box.prop( active_mat.cv_data, "ocean_colour" )
2511 #}
2512 #}
2513 #}
2514
2515 class CV_OBJ_PANEL(bpy.types.Panel):
2516 #{
2517 bl_label="Entity Config"
2518 bl_idname="SCENE_PT_cv_entity"
2519 bl_space_type='PROPERTIES'
2520 bl_region_type='WINDOW'
2521 bl_context="object"
2522
2523 def draw(_,context):
2524 #{
2525 active_object = bpy.context.active_object
2526 if active_object == None: return
2527 if active_object.type == 'ARMATURE':
2528 #{
2529 row = _.layout.row()
2530 row.enabled = False
2531 row.label( text="This object has the intrinsic classtype of skeleton" )
2532 return
2533 #}
2534
2535 _.layout.prop( active_object.cv_data, "classtype" )
2536
2537 classtype = active_object.cv_data.classtype
2538
2539 if (classtype != 'classtype_none') and (classtype in globals()):
2540 #{
2541 cl = globals()[ classtype ]
2542
2543 if getattr( cl, "editor_interface", None ):
2544 #{
2545 cl.editor_interface( _.layout, active_object )
2546 #}
2547 #}
2548 #}
2549 #}
2550
2551 class CV_COMPILE(bpy.types.Operator):
2552 #{
2553 bl_idname="carve.compile_all"
2554 bl_label="Compile All"
2555
2556 def execute(_,context):
2557 #{
2558 view_layer = bpy.context.view_layer
2559 for col in view_layer.layer_collection.children["export"].children:
2560 if not col.hide_viewport or bpy.context.scene.cv_data.use_hidden:
2561 write_model( col.name )
2562
2563 return {'FINISHED'}
2564 #}
2565 #}
2566
2567 class CV_COMPILE_THIS(bpy.types.Operator):
2568 #{
2569 bl_idname="carve.compile_this"
2570 bl_label="Compile This collection"
2571
2572 def execute(_,context):
2573 #{
2574 col = bpy.context.collection
2575 write_model( col.name )
2576
2577 return {'FINISHED'}
2578 #}
2579 #}
2580
2581 class CV_INTERFACE(bpy.types.Panel):
2582 #{
2583 bl_idname = "VIEW3D_PT_carve"
2584 bl_label = "Skate Rift"
2585 bl_space_type = 'VIEW_3D'
2586 bl_region_type = 'UI'
2587 bl_category = "Skate Rift"
2588
2589 def draw(_, context):
2590 #{
2591 layout = _.layout
2592 layout.prop( context.scene.cv_data, "export_dir" )
2593
2594 col = bpy.context.collection
2595
2596 found_in_export = False
2597 export_count = 0
2598 view_layer = bpy.context.view_layer
2599 for c1 in view_layer.layer_collection.children["export"].children:
2600 #{
2601 if not c1.hide_viewport or bpy.context.scene.cv_data.use_hidden:
2602 export_count += 1
2603
2604 if c1.name == col.name:
2605 #{
2606 found_in_export = True
2607 #}
2608 #}
2609
2610 box = layout.box()
2611 if found_in_export:
2612 #{
2613 box.label( text=col.name + ".mdl" )
2614 box.prop( col.cv_data, "pack_textures" )
2615 box.operator( "carve.compile_this" )
2616 #}
2617 else:
2618 #{
2619 row = box.row()
2620 row.enabled=False
2621 row.label( text=col.name )
2622 box.label( text="This collection is not in the export group" )
2623 #}
2624
2625 box = layout.box()
2626 row = box.row()
2627
2628 split = row.split( factor = 0.3, align=True )
2629 split.prop( context.scene.cv_data, "use_hidden", text="hidden" )
2630
2631 row1 = split.row()
2632 if export_count == 0:
2633 row1.enabled=False
2634 row1.operator( "carve.compile_all", \
2635 text=F"Compile all ({export_count} collections)" )
2636 #}
2637 #}
2638
2639
2640 classes = [CV_OBJ_SETTINGS,CV_OBJ_PANEL,CV_COMPILE,CV_INTERFACE,\
2641 CV_MESH_SETTINGS, CV_SCENE_SETTINGS, CV_BONE_SETTINGS,\
2642 CV_BONE_PANEL, CV_COLLECTION_SETTINGS, CV_COMPILE_THIS,\
2643 CV_MATERIAL_SETTINGS, CV_MATERIAL_PANEL ]
2644
2645 def register():
2646 #{
2647 global cv_view_draw_handler
2648
2649 for c in classes:
2650 bpy.utils.register_class(c)
2651
2652 bpy.types.Object.cv_data = bpy.props.PointerProperty(type=CV_OBJ_SETTINGS)
2653 bpy.types.Mesh.cv_data = bpy.props.PointerProperty(type=CV_MESH_SETTINGS)
2654 bpy.types.Scene.cv_data = bpy.props.PointerProperty(type=CV_SCENE_SETTINGS)
2655 bpy.types.Bone.cv_data = bpy.props.PointerProperty(type=CV_BONE_SETTINGS)
2656 bpy.types.Collection.cv_data = \
2657 bpy.props.PointerProperty(type=CV_COLLECTION_SETTINGS)
2658 bpy.types.Material.cv_data = \
2659 bpy.props.PointerProperty(type=CV_MATERIAL_SETTINGS)
2660
2661 cv_view_draw_handler = bpy.types.SpaceView3D.draw_handler_add(\
2662 cv_draw,(),'WINDOW','POST_VIEW')
2663 #}
2664
2665 def unregister():
2666 #{
2667 global cv_view_draw_handler
2668
2669 for c in classes:
2670 bpy.utils.unregister_class(c)
2671
2672 bpy.types.SpaceView3D.draw_handler_remove(cv_view_draw_handler,'WINDOW')
2673 #}
2674
2675 # ---------------------------------------------------------------------------- #
2676 # #
2677 # QOI encoder #
2678 # #
2679 # ---------------------------------------------------------------------------- #
2680 # #
2681 # Transliteration of: #
2682 # https://github.com/phoboslab/qoi/blob/master/qoi.h #
2683 # #
2684 # Copyright (c) 2021, Dominic Szablewski - https://phoboslab.org #
2685 # SPDX-License-Identifier: MIT #
2686 # QOI - The "Quite OK Image" format for fast, lossless image compression #
2687 # #
2688 # ---------------------------------------------------------------------------- #
2689
2690 class qoi_rgba_t(Structure):
2691 #{
2692 _pack_ = 1
2693 _fields_ = [("r",c_uint8),
2694 ("g",c_uint8),
2695 ("b",c_uint8),
2696 ("a",c_uint8)]
2697 #}
2698
2699 QOI_OP_INDEX = 0x00 # 00xxxxxx
2700 QOI_OP_DIFF = 0x40 # 01xxxxxx
2701 QOI_OP_LUMA = 0x80 # 10xxxxxx
2702 QOI_OP_RUN = 0xc0 # 11xxxxxx
2703 QOI_OP_RGB = 0xfe # 11111110
2704 QOI_OP_RGBA = 0xff # 11111111
2705
2706 QOI_MASK_2 = 0xc0 # 11000000
2707
2708 def qoi_colour_hash( c ):
2709 #{
2710 return c.r*3 + c.g*5 + c.b*7 + c.a*11
2711 #}
2712
2713 def qoi_eq( a, b ):
2714 #{
2715 return (a.r==b.r) and (a.g==b.g) and (a.b==b.b) and (a.a==b.a)
2716 #}
2717
2718 def qoi_32bit( v ):
2719 #{
2720 return bytearray([ (0xff000000 & v) >> 24, \
2721 (0x00ff0000 & v) >> 16, \
2722 (0x0000ff00 & v) >> 8, \
2723 (0x000000ff & v) ])
2724 #}
2725
2726 def qoi_encode( img ):
2727 #{
2728 data = bytearray()
2729
2730 print(F" . Encoding {img.name}.qoi[{img.size[0]},{img.size[1]}]")
2731
2732 index = [ qoi_rgba_t() for _ in range(64) ]
2733
2734 # Header
2735 #
2736 data.extend( bytearray(c_uint32(0x66696f71)) )
2737 data.extend( qoi_32bit( img.size[0] ) )
2738 data.extend( qoi_32bit( img.size[1] ) )
2739 data.extend( bytearray(c_uint8(4)) )
2740 data.extend( bytearray(c_uint8(0)) )
2741
2742 run = 0
2743 px_prev = qoi_rgba_t()
2744 px_prev.r = c_uint8(0)
2745 px_prev.g = c_uint8(0)
2746 px_prev.b = c_uint8(0)
2747 px_prev.a = c_uint8(255)
2748
2749 px = qoi_rgba_t()
2750 px.r = c_uint8(0)
2751 px.g = c_uint8(0)
2752 px.b = c_uint8(0)
2753 px.a = c_uint8(255)
2754
2755 px_len = img.size[0] * img.size[1]
2756
2757 paxels = [ int(min(max(_,0),1)*255) for _ in img.pixels ]
2758
2759 for px_pos in range( px_len ):
2760 #{
2761 idx = px_pos * img.channels
2762 nc = img.channels-1
2763
2764 px.r = paxels[idx+min(0,nc)]
2765 px.g = paxels[idx+min(1,nc)]
2766 px.b = paxels[idx+min(2,nc)]
2767 px.a = paxels[idx+min(3,nc)]
2768
2769 if qoi_eq( px, px_prev ):
2770 #{
2771 run += 1
2772
2773 if (run == 62) or (px_pos == px_len-1):
2774 #{
2775 data.extend( bytearray( c_uint8(QOI_OP_RUN | (run-1))) )
2776 run = 0
2777 #}
2778 #}
2779 else:
2780 #{
2781 if run > 0:
2782 #{
2783 data.extend( bytearray( c_uint8(QOI_OP_RUN | (run-1))) )
2784 run = 0
2785 #}
2786
2787 index_pos = qoi_colour_hash(px) % 64
2788
2789 if qoi_eq( index[index_pos], px ):
2790 #{
2791 data.extend( bytearray( c_uint8(QOI_OP_INDEX | index_pos)) )
2792 #}
2793 else:
2794 #{
2795 index[ index_pos ].r = px.r
2796 index[ index_pos ].g = px.g
2797 index[ index_pos ].b = px.b
2798 index[ index_pos ].a = px.a
2799
2800 if px.a == px_prev.a:
2801 #{
2802 vr = int(px.r) - int(px_prev.r)
2803 vg = int(px.g) - int(px_prev.g)
2804 vb = int(px.b) - int(px_prev.b)
2805
2806 vg_r = vr - vg
2807 vg_b = vb - vg
2808
2809 if (vr > -3) and (vr < 2) and\
2810 (vg > -3) and (vg < 2) and\
2811 (vb > -3) and (vb < 2):
2812 #{
2813 op = QOI_OP_DIFF | (vr+2) << 4 | (vg+2) << 2 | (vb+2)
2814 data.extend( bytearray( c_uint8(op) ))
2815 #}
2816 elif (vg_r > -9) and (vg_r < 8) and\
2817 (vg > -33) and (vg < 32 ) and\
2818 (vg_b > -9) and (vg_b < 8):
2819 #{
2820 op = QOI_OP_LUMA | (vg+32)
2821 delta = (vg_r+8) << 4 | (vg_b + 8)
2822 data.extend( bytearray( c_uint8(op) ) )
2823 data.extend( bytearray( c_uint8(delta) ))
2824 #}
2825 else:
2826 #{
2827 data.extend( bytearray( c_uint8(QOI_OP_RGB) ) )
2828 data.extend( bytearray( c_uint8(px.r) ))
2829 data.extend( bytearray( c_uint8(px.g) ))
2830 data.extend( bytearray( c_uint8(px.b) ))
2831 #}
2832 #}
2833 else:
2834 #{
2835 data.extend( bytearray( c_uint8(QOI_OP_RGBA) ) )
2836 data.extend( bytearray( c_uint8(px.r) ))
2837 data.extend( bytearray( c_uint8(px.g) ))
2838 data.extend( bytearray( c_uint8(px.b) ))
2839 data.extend( bytearray( c_uint8(px.a) ))
2840 #}
2841 #}
2842 #}
2843
2844 px_prev.r = px.r
2845 px_prev.g = px.g
2846 px_prev.b = px.b
2847 px_prev.a = px.a
2848 #}
2849
2850 # Padding
2851 for i in range(7):
2852 data.extend( bytearray( c_uint8(0) ))
2853 data.extend( bytearray( c_uint8(1) ))
2854 bytearray_align_to( data, 16, 0 )
2855
2856 return data
2857 #}