1+ import pyvista as pv
2+ import numpy as np
3+ import struct
4+
5+ def load_ff7_model (filename ):
6+ with open (filename , 'rb' ) as f :
7+ data = bytearray (f .read ())
8+ num_v = struct .unpack ('<I' , data [12 :16 ])[0 ]
9+ num_n = struct .unpack ('<I' , data [16 :20 ])[0 ]
10+ num_t = struct .unpack ('<I' , data [24 :28 ])[0 ]
11+ num_c = struct .unpack ('<I' , data [28 :32 ])[0 ]
12+
13+ vertex_start = 128
14+ verts = np .frombuffer (data [vertex_start : vertex_start + (num_v * 12 )], dtype = np .float32 ).reshape (- 1 , 3 )
15+
16+ offset = 128 + (num_v * 12 ) + (num_n * 24 ) + (num_t * 8 )
17+ color_raw = data [offset : offset + (num_c * 4 )]
18+ color_data = np .frombuffer (color_raw , dtype = np .uint8 ).reshape (- 1 , 4 )
19+ rgb = color_data [:, [2 , 1 , 0 ]].copy ()
20+
21+ return verts , rgb , data , offset , num_c
22+
23+ # --- Global State ---
24+ FILE_NAME = 'ruam'
25+ verts , current_rgb , full_raw_data , color_offset , color_count = load_ff7_model (FILE_NAME )
26+ original_rgb = current_rgb .copy ()
27+ modified_mask = np .zeros (color_count , dtype = bool )
28+
29+ colors = [
30+ np .array ([156 , 114 , 102 ], dtype = np .int16 ),
31+ np .array ([200 , 150 , 120 ], dtype = np .int16 )
32+ ]
33+ active_slot = 0
34+ restrict_changes = True
35+ point_size_val = 100.0
36+
37+ plotter = pv .Plotter (window_size = [1100 , 800 ])
38+ cloud = pv .PolyData (verts )
39+ cloud ['colors' ] = current_rgb
40+ plotter .background_color = "white"
41+
42+ def refresh_mesh ():
43+ cloud ['colors' ] = current_rgb
44+ plotter .add_mesh (cloud , scalars = 'colors' , rgb = True , render_points_as_spheres = True ,
45+ point_size = point_size_val , name = "model" )
46+
47+ # --- Dynamic UI Updates ---
48+
49+ def update_color_displays ():
50+ """Updates the numeric labels and the color box color."""
51+ gui_x = 20
52+ labels = ["R" , "G" , "B" ]
53+
54+ for i in range (2 ):
55+ y_offset = 740 if i == 0 else 600
56+ # Update Text Labels (using 'name' ensures it replaces the old text instantly)
57+ for c in range (3 ):
58+ y = y_offset - (c * 25 )
59+ plotter .add_text (f"{ labels [c ]} : { colors [i ][c ]} " , position = (gui_x , y ),
60+ font_size = 9 , color = "black" , name = f"txt_{ i } _{ c } " )
61+
62+ # Update the Large Color Box
63+ # We recreate the widget only when the color changes to avoid stacking
64+ border = "yellow" if active_slot == i else "black"
65+ plotter .add_checkbox_button_widget (
66+ lambda b , idx = i : set_active_selection (idx ),
67+ value = True , position = (135 , y_offset - 45 ), size = 65 ,
68+ color_on = colors [i ].astype (float )/ 255.0 ,
69+ color_off = colors [i ].astype (float )/ 255.0 ,
70+ background_color = border
71+ )
72+
73+ def adjust_rgb (slot , channel , delta ):
74+ """Adjusts values and updates only what is necessary (Instant)."""
75+ colors [slot ][channel ] = np .clip (colors [slot ][channel ] + delta , 0 , 255 )
76+ update_color_displays ()
77+
78+ def set_active_selection (slot ):
79+ global active_slot
80+ active_slot = slot
81+ update_color_displays ()
82+
83+ # --- Initial UI Construction (Run ONCE) ---
84+
85+ def build_static_ui ():
86+ gui_x = 20
87+ # 1. Create adjustment buttons once
88+ for i in range (2 ):
89+ y_offset = 740 if i == 0 else 600
90+ for channel in range (3 ):
91+ y = y_offset - (channel * 25 )
92+
93+ # Plus (White background to hide gray box)
94+ plotter .add_checkbox_button_widget (lambda b , s = i , c = channel : adjust_rgb (s , c , 5 ),
95+ position = (gui_x + 65 , y ), size = 18 , color_on = "white" , color_off = "white" , background_color = "white" )
96+ plotter .add_text ("+" , position = (gui_x + 68 , y + 2 ), font_size = 8 , color = "black" )
97+
98+ # Minus
99+ plotter .add_checkbox_button_widget (lambda b , s = i , c = channel : adjust_rgb (s , c , - 5 ),
100+ position = (gui_x + 85 , y ), size = 18 , color_on = "white" , color_off = "white" , background_color = "white" )
101+ plotter .add_text ("-" , position = (gui_x + 89 , y + 2 ), font_size = 8 , color = "black" )
102+
103+ # 2. Create Action Buttons once
104+ y_base = 380
105+ # Tint All
106+ plotter .add_checkbox_button_widget (lambda b : apply_tint_all (), position = (gui_x , y_base ), size = 40 , color_on = "lightgrey" )
107+ plotter .add_text ("Tint ALL" , position = (gui_x + 50 , y_base + 10 ), font_size = 10 , color = "black" )
108+ # Lighten
109+ plotter .add_checkbox_button_widget (lambda b : adjust_brightness (1.1 ), position = (gui_x , y_base - 70 ), size = 40 , color_on = "lightgrey" )
110+ plotter .add_text ("Lighten All" , position = (gui_x + 50 , y_base - 60 ), font_size = 10 , color = "black" )
111+ # Darken (Restored)
112+ plotter .add_checkbox_button_widget (lambda b : adjust_brightness (0.9 ), position = (gui_x , y_base - 140 ), size = 40 , color_on = "lightgrey" )
113+ plotter .add_text ("Darken All" , position = (gui_x + 50 , y_base - 130 ), font_size = 10 , color = "black" )
114+ # Save
115+ plotter .add_checkbox_button_widget (lambda b : save_model (), position = (gui_x , y_base - 210 ), size = 40 , color_on = "lightgrey" )
116+ plotter .add_text ("Save" , position = (gui_x + 50 , y_base - 200 ), font_size = 10 , color = "black" )
117+
118+ # 3. Restriction Toggle
119+ plotter .add_checkbox_button_widget (toggle_restriction , value = restrict_changes , position = (gui_x , 50 ), size = 30 )
120+ plotter .add_text ("restrict to 1 change each" , position = (gui_x + 40 , 55 ), font_size = 9 , color = "black" )
121+
122+ # 4. Point Size Slider
123+ plotter .add_slider_widget (update_point_size , [1 , 250 ], value = 100 , title = "" ,
124+ pointa = (0.6 , 0.05 ), pointb = (0.9 , 0.05 ))
125+
126+ # --- Logic Actions ---
127+
128+ def apply_tint_all ():
129+ global current_rgb
130+ tint = colors [active_slot ].astype (float ) / 255.0
131+ for i in range (color_count ):
132+ if restrict_changes and modified_mask [i ]: continue
133+ current_rgb [i ] = np .clip (original_rgb [i ] * tint , 0 , 255 ).astype (np .uint8 )
134+ modified_mask [i ] = True
135+ refresh_mesh ()
136+
137+ def adjust_brightness (factor ):
138+ global current_rgb
139+ indices = np .where (modified_mask )[0 ]
140+ for i in indices :
141+ current_rgb [i ] = np .clip (current_rgb [i ].astype (float ) * factor , 0 , 255 ).astype (np .uint8 )
142+ refresh_mesh ()
143+
144+ def toggle_restriction (state ):
145+ global restrict_changes
146+ restrict_changes = state
147+
148+ def update_point_size (value ):
149+ global point_size_val
150+ point_size_val = value
151+ refresh_mesh ()
152+
153+ def save_model ():
154+ for i in range (color_count ):
155+ pos = color_offset + (i * 4 )
156+ full_raw_data [pos :pos + 3 ] = current_rgb [i , [2 , 1 , 0 ]]
157+ full_raw_data [pos + 3 ] = 255
158+ with open (f'{ FILE_NAME } _shaded.p' , 'wb' ) as f :
159+ f .write (full_raw_data )
160+ print ("Export successful." )
161+
162+ def on_click (point ):
163+ idx = cloud .find_closest_point (point )
164+ if restrict_changes and modified_mask [idx ]: return
165+ tint = colors [active_slot ].astype (float ) / 255.0
166+ current_rgb [idx ] = np .clip (original_rgb [idx ] * tint , 0 , 255 ).astype (np .uint8 )
167+ modified_mask [idx ] = True
168+ refresh_mesh ()
169+
170+ # --- Execution ---
171+ refresh_mesh ()
172+ build_static_ui ()
173+ update_color_displays () # Build initial dynamic labels
174+ plotter .enable_point_picking (callback = on_click , show_message = False , color = 'yellow' )
175+ plotter .show ()
0 commit comments