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