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