-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathopengl_canvas.py
More file actions
2577 lines (2175 loc) · 113 KB
/
opengl_canvas.py
File metadata and controls
2577 lines (2175 loc) · 113 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from PyQt5.QtWidgets import QOpenGLWidget, QMessageBox, QMainWindow
from PyQt5.QtCore import Qt, QPoint, QSize, pyqtSignal, QTimer, QPointF, QSizeF
from PyQt5.QtGui import QMouseEvent, QTabletEvent, QImage, QVector2D, QCursor
from OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.error import GLError
from PIL import Image
import numpy as np
import ctypes
import time
import enum
import os
# 鼠标状态枚举,用于优化状态检查
class MouseState(enum.Enum):
IDLE = 0 # 空闲状态
DRAWING = 1 # 正常绘制
ERASING = 2 # 橡皮擦模式
DRAG_PREVIEW = 3 # 拖拽预览视图
DRAG_MAIN = 4 # 拖拽主视图
# 笔刷数据类,缓存常用的计算结果
class BrushData:
def __init__(self):
self.center_x = 0 # 笔刷中心X坐标
self.center_y = 0 # 笔刷中心Y坐标
self.radius = 0 # 笔刷半径
self.min_x = 0 # 影响区域最小X
self.max_x = 0 # 影响区域最大X
self.min_y = 0 # 影响区域最小Y
self.max_y = 0 # 影响区域最大Y
self.flow_r = 0.5 # 红色分量
self.flow_g = 0.5 # 绿色分量
self.strength = 0.0 # 笔刷强度
self.needs_seamless = False # 是否需要四方连续处理
self.mirror_positions = [] # 需要镜像的位置列表
self.dist_sq_cache = None # 距离场缓存
self.falloff_cache = None # 衰减系数缓存
# 基础顶点着色器
VERTEX_SHADER_SOURCE = """
#version 150
in vec2 aPos;
in vec2 aTexCoords;
out vec2 TexCoords;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
TexCoords = aTexCoords;
}
"""
PREVIEW_FRAGMENT_SHADER_SOURCE = None
# --- Helper Function for Shader Compilation ---
def create_shader_program(vertex_source, fragment_source):
"""编译顶点和片段着色器,并链接成一个程序"""
try:
vertex_shader = shaders.compileShader(vertex_source, GL_VERTEX_SHADER)
fragment_shader = shaders.compileShader(fragment_source, GL_FRAGMENT_SHADER)
# 注意:PyOpenGL 的 compileProgram 会自动处理附加和链接
# 它在失败时会引发 RuntimeError,并通常包含日志信息
program = shaders.compileProgram(vertex_shader, fragment_shader)
# 编译后可以立即删除着色器对象 (它们已链接到程序中)
glDeleteShader(vertex_shader)
glDeleteShader(fragment_shader)
print(f"Shader program compiled and linked successfully. ID: {program}")
return program
except Exception as e:
# shaders.compileProgram 会在日志中打印错误,但我们再次打印异常信息
print(f"Shader compilation/linking failed: {e}")
# 尝试访问日志(如果存在)
log = None
if hasattr(e, 'log') and e.log:
log = e.log.decode(errors='ignore') # Decode bytes log, ignore errors
print("--- Shader Log ---")
print(log)
print("------------------")
# 可以尝试更详细地检查哪个阶段失败,但 compileProgram 通常足够
return 0 # 返回 0 表示失败
class FlowmapCanvas(QOpenGLWidget):
# 信号,当 flowmap 更新时发出,用于更新预览等
flowmap_updated = pyqtSignal()
mouseMoveNonDrawing = pyqtSignal(QPointF) # 鼠标移动但没有绘制时发出
drawingStarted = pyqtSignal() # 开始绘制时发出
drawingFinished = pyqtSignal() # 结束绘制时发出
mouse_moved = pyqtSignal(QPoint)
resized = pyqtSignal()
opengl_initialized = pyqtSignal() # 新增信号,当OpenGL初始化完成时发出
base_image_loaded = pyqtSignal(int, int) # 新增信号,当底图加载完成时发出,参数为宽度和高度
brush_properties_changed = pyqtSignal(float, float) # 新增信号,当笔刷属性(半径、强度)变化时发出
hover_entered = pyqtSignal()
hover_left = pyqtSignal()
def __init__(self, parent=None, size=(1024, 1024)):
super().__init__(parent)
self.texture_size = size
self.last_pos = QPoint()
self.mouse_state = MouseState.IDLE # 使用枚举代替多个布尔标志
self.brush_radius = 40.0 # 笔刷半径 (像素)
self.brush_strength = 0.5 # 笔刷强度 [0, 1]
self.speed_sensitivity = 0.7 # 鼠标速度灵敏度 [0, 1]
# 数位板笔压相关属性
self.current_pressure = 1.0 # 当前笔压 [0, 1]
self.pressure_affects_size = True # 笔压是否影响笔刷大小
self.pressure_affects_strength = True # 笔压是否影响笔刷强度
self.base_brush_radius = 40.0 # 基础笔刷半径(无压感时)
self.base_brush_strength = 0.5 # 基础笔刷强度(无压感时)
self.is_tablet_input = False # 标记当前是否为数位板输入
# 笔压响应参数(可配置)
self.pressure_size_min = 0.2 # 大小最小值(基础大小的百分比)
self.pressure_strength_min = 0.2 # 强度最小值(基础强度的百分比)
self.graphics_api_mode = "opengl" # 默认使用OpenGL模式 - 'opengl'或'directx'
self.enable_seamless = False # 启用四方连续贴图
self.preview_size = QSizeF(0.2, 0.2) # 预览窗口的初始大小
self.preview_offset = QPointF(0.0, 0.0) # 预览窗口中内容的偏移量(归一化坐标)
self.preview_repeat = False # 启用底图重复显示
self.is_dragging_preview = False # 拖拽预览视角
self.last_mouse_pos = QPoint() # 上一次鼠标位置,用于拖拽预览
# Shift键状态 - 用于模糊效果
self.shift_pressed = False
self.s_pressed = False # S键状态(用于笔刷调整)
self.s_press_position = None # S键按下时的位置
self.initial_brush_radius = 40.0 # 按下S键时的初始笔刷半径
self.initial_brush_strength = 0.5 # 按下S键时的初始笔刷强度
# 绘制优化参数
self.last_draw_time = 0 # 上次绘制时间
self.draw_throttle_ms = 16 # 绘制节流时间,约60fps
self.accumulated_positions = [] # 累积的位置,用于节流期间保存鼠标位置
self.update_pending = False # 是否有待处理的更新
# 主视图控制
self.main_view_scale = 1.0 # 主视图缩放比例
self.target_main_view_scale = 1.0 # 目标主视图缩放比例(用于平滑过渡)
self.main_view_offset = QPointF(0.0, 0.0) # 主视图偏移量
self.target_main_view_offset = QPointF(0.0, 0.0) # 目标主视图偏移量(用于平滑过渡)
self.is_dragging_main_view = False # 拖拽主视图标志
self.scale_animation_active = False # 缩放动画是否激活
self.scale_animation_start_time = 0 # 缩放动画开始时间
self.scale_animation_duration = 0.15 # 缩放动画持续时间(秒)
self.MAX_SCALE = 20.0 # 最大缩放倍数
self.MIN_SCALE = 0.05 # 最小缩放倍数
self.SCROLL_SENSITIVITY = 0.25 # 滚轮灵敏度,值越小越不敏感
# 纵横比校正参数
self.texture_original_aspect_ratio = 1.0 # 纹理的原始纵横比
self.main_view_scale_correction_x = 1.0 # X方向的缩放校正
self.main_view_scale_correction_y = 1.0 # Y方向的缩放校正
self.main_view_offset_correction_x = 0.0 # X方向的偏移校正
self.main_view_offset_correction_y = 0.0 # Y方向的偏移校正
self.preview_aspect_ratio = 1.0 # 预览窗口的宽高比,默认为1:1
# cover 模式下的屏幕->内容校正参数(传给shader)
self.aspect_scale_x = 1.0
self.aspect_scale_y = 1.0
self.aspect_offset_x = 0.0
self.aspect_offset_y = 0.0
# Flowmap 数据 (H, W, RGBA)
self.flowmap_data = np.zeros((self.texture_size[1], self.texture_size[0], 4), dtype=np.float32)
self.flowmap_data[..., 0] = 0.5
self.flowmap_data[..., 1] = 0.5
self.flowmap_data[..., 3] = 1.0
self.flowmap_texture_id = 0
self.base_texture_id = 0
self.has_base_map = False
self.shader_program_id = 0
self.preview_shader_program_id = 0
self.overlay_shader_program_id = 0
self.overlay_texture_id = 0
self.overlay_opacity = 0.5
self.has_overlay = False
# UV overlay state
self.uv_overlay_tex = 0
self.uv_overlay_enabled = False
self.uv_overlay_opacity = 0.7
self.vao = 0
self.vbo = 0
# 顶点数据 (全屏四边形)
self.quad_vertices = np.array([
-1.0, 1.0, 0.0, 1.0,
-1.0, -1.0, 0.0, 0.0,
1.0, 1.0, 1.0, 1.0,
1.0, -1.0, 1.0, 0.0,
], dtype=np.float32)
# 3D 模型相关(保留)
self.uv_data = None
self.uv_vbo = 0
# Flow effect parameters
self.flow_speed = 0.5
self.flow_distortion = 0.3
self.base_scale = 1.0
self.anim_time = 0.0
self.last_anim_update_time = time.time()
self.is_animating = True
self.start_time = time.time()
# Timer for animation update
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_animation)
self.timer.start(16)
self.setFocusPolicy(Qt.StrongFocus)
self.setMouseTracking(True)
self.brush_data = BrushData()
self.is_drawing = False
self.is_erasing = False
self.is_dragging_preview = False
self.is_dragging_main_view = False
self.uv_wire_program = 0
self.uv_wire_vao = 0
self.uv_wire_vbo = 0
self.uv_wire_ebo = 0
self.uv_wire_index_count = 0
self.uv_wire_opacity = 0.7
self.uv_wire_enabled = False
self.uv_wire_line_width = 1.0
def update_animation(self):
"""更新动画状态"""
current_time = time.time()
delta_time = current_time - self.last_anim_update_time
self.last_anim_update_time = current_time
# 累积动画时间
self.anim_time += delta_time
# 处理缩放动画
if self.scale_animation_active:
progress = min(1.0, (current_time - self.scale_animation_start_time) / self.scale_animation_duration)
if progress >= 1.0:
self.main_view_scale = self.target_main_view_scale
self.main_view_offset = self.target_main_view_offset
self.scale_animation_active = False
else:
# 使用平滑的缓动函数
t = progress
ease = t * t * (3.0 - 2.0 * t) # 平滑过渡函数
# 插值当前值和目标值
self.main_view_scale = self.main_view_scale + (self.target_main_view_scale - self.main_view_scale) * ease
# 对偏移量进行插值
self.main_view_offset = QPointF(
self.main_view_offset.x() + (self.target_main_view_offset.x() - self.main_view_offset.x()) * ease,
self.main_view_offset.y() + (self.target_main_view_offset.y() - self.main_view_offset.y()) * ease
)
# 强制每帧更新,确保动画流畅
self.update()
def initializeGL(self):
"""初始化OpenGL上下文"""
try:
print("Initializing OpenGL context...")
print("OpenGL Version:", glGetString(GL_VERSION).decode(errors='ignore'))
print("GLSL Version:", glGetString(GL_SHADING_LANGUAGE_VERSION).decode(errors='ignore'))
print("Vendor:", glGetString(GL_VENDOR).decode(errors='ignore'))
print("Renderer:", glGetString(GL_RENDERER).decode(errors='ignore'))
# 设置清空颜色
glClearColor(0.1, 0.1, 0.1, 1.0)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
# 启用模板缓冲支持
glClearStencil(0)
# 初始化顶点缓冲对象
self.init_quad_buffers()
# 初始化纹理
self.init_textures()
# 初始化着色器
self.init_shaders()
# 设置初始宽高比
self.texture_original_aspect_ratio = self.texture_size[0] / self.texture_size[1]
self.window_width = self.width() or 800
self.window_height = self.height() or 600
# 更新预览窗口大小
self.update_preview_size()
# 设置动画计时器
self.start_time = time.time()
self.last_anim_update_time = time.time()
# 发送OpenGL初始化完成信号
self.opengl_initialized.emit()
# 初始化完成后,立即强制更新一次
self.update_aspect_ratio()
self.update()
print("OpenGL initialization completed successfully!")
except Exception as e:
print(f"OpenGL initialization error: {e}")
import traceback
traceback.print_exc()
def init_quad_buffers(self):
# 使用 gl* 函数
vao_id = glGenVertexArrays(1)
self.vao = vao_id
vbo_id = glGenBuffers(1)
self.vbo = vbo_id
glBindVertexArray(self.vao)
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
glBufferData(GL_ARRAY_BUFFER, self.quad_vertices.nbytes, self.quad_vertices, GL_STATIC_DRAW)
# 位置属性 (location = 0)
# 使用 ctypes.c_void_p 进行偏移
# sizeof(ctypes.c_float) is 4
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * 4, ctypes.c_void_p(0))
glEnableVertexAttribArray(0)
# 纹理坐标属性 (location = 1)
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * 4, ctypes.c_void_p(2 * 4))
glEnableVertexAttribArray(1)
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)
def init_textures(self):
# 使用 gl* 函数
# --- Flowmap Texture ---
texture_id = glGenTextures(1)
self.flowmap_texture_id = texture_id
glBindTexture(GL_TEXTURE_2D, self.flowmap_texture_id)
# 使用REPEAT模式以支持无缝贴图
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F,
self.texture_size[0], self.texture_size[1], 0,
GL_RGBA, GL_FLOAT, self.flowmap_data)
# --- Base Texture (Placeholder) ---
texture_id = glGenTextures(1)
self.base_texture_id = texture_id
glBindTexture(GL_TEXTURE_2D, self.base_texture_id)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
white_pixel = np.array([[[128, 128, 128, 255]]], dtype=np.uint8) # Use grey placeholder
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, white_pixel)
glBindTexture(GL_TEXTURE_2D, 0)
def load_overlay_image(self, file_path):
"""加载参考贴图到GPU纹理"""
try:
self.makeCurrent()
img = Image.open(file_path).convert('RGBA')
width, height = img.size
data = np.array(img, dtype=np.uint8)
data = np.flipud(data)
if self.overlay_texture_id == 0:
self.overlay_texture_id = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, self.overlay_texture_id)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glBindTexture(GL_TEXTURE_2D, 0)
self.has_overlay = True
self.doneCurrent()
self.update()
return True
except Exception as e:
print(f"Failed to load overlay image: {e}")
try:
glBindTexture(GL_TEXTURE_2D, 0)
except:
pass
self.has_overlay = False
self.doneCurrent()
return False
def init_shaders(self):
# 使用辅助函数创建 shader program
fragShader = ""
with open("shaders/flow_shader.glsl", encoding="utf-8") as f:
fragShader = f.read()
self.shader_program_id = create_shader_program(VERTEX_SHADER_SOURCE, fragShader)
if self.shader_program_id == 0:
QMessageBox.critical(self, "Shader Error", "Failed to compile or link shaders. Check console output.")
# 创建预览 shader(从外部文件加载)
try:
with open("shaders/preview_shader.glsl", encoding="utf-8") as f:
preview_frag_source = f.read()
except Exception as e:
print(f"Failed to read preview_shader.glsl: {e}")
preview_frag_source = None
if preview_frag_source:
self.preview_shader_program_id = create_shader_program(VERTEX_SHADER_SOURCE, preview_frag_source)
else:
self.preview_shader_program_id = 0
if self.preview_shader_program_id == 0:
QMessageBox.critical(self, "Shader Error", "Failed to compile preview shader.")
# Overlay Texture Shader
try:
with open("shaders/overlay_shader.glsl", encoding="utf-8") as f:
self.overlay_shader_program_id = create_shader_program(VERTEX_SHADER_SOURCE, f.read())
except Exception as e:
print(f"Failed to read overlay_texture_shader.glsl: {e}")
self.overlay_shader_program_id = 0
# UV wire program
try:
with open("shaders/uv_wire_vs.glsl", encoding="utf-8") as f:
uv_vs = f.read()
with open("shaders/uv_wire_ps.glsl", encoding="utf-8") as f:
uv_fs = f.read()
self.uv_wire_program = create_shader_program(uv_vs, uv_fs)
except Exception as e:
print(f"Failed to read UV wire shaders: {e}")
self.uv_wire_program = 0
def paintGL(self):
"""绘制OpenGL内容"""
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# 检查 shader program ID 和 VAO 是否有效
if self.shader_program_id == 0 or self.preview_shader_program_id == 0 or self.vao == 0:
return
# draw main shader full-screen
try:
glViewport(0, 0, self.width(), self.height())
glUseProgram(self.shader_program_id)
# --- 获取 Uniform 位置并设置 ---
baseMapLoc = glGetUniformLocation(self.shader_program_id, "baseMap")
hasBaseMapLoc = glGetUniformLocation(self.shader_program_id, "u_hasBaseMap")
flowMapLoc = glGetUniformLocation(self.shader_program_id, "flowMap")
timeLoc = glGetUniformLocation(self.shader_program_id, "u_time")
speedLoc = glGetUniformLocation(self.shader_program_id, "u_flowSpeed")
distLoc = glGetUniformLocation(self.shader_program_id, "u_flowDistortion")
baseScaleLoc = glGetUniformLocation(self.shader_program_id, "u_scale")
previewRepeatLoc = glGetUniformLocation(self.shader_program_id, "u_previewRepeat")
mainViewScaleLoc = glGetUniformLocation(self.shader_program_id, "u_mainViewScale")
mainViewOffsetLoc = glGetUniformLocation(self.shader_program_id, "u_mainViewOffset")
useDirectX = glGetUniformLocation(self.shader_program_id, "u_useDirectX")
glActiveTexture(GL_TEXTURE0)
base_tex_to_bind = self.base_texture_id if self.base_texture_id != 0 else 0
glBindTexture(GL_TEXTURE_2D, base_tex_to_bind)
if baseMapLoc != -1:
glUniform1i(baseMapLoc, 0)
has_valid_base = self.has_base_map and self.base_texture_id != 0
if hasBaseMapLoc != -1:
glUniform1i(hasBaseMapLoc, 1 if has_valid_base else 0)
glActiveTexture(GL_TEXTURE1)
flow_tex_to_bind = self.flowmap_texture_id if self.flowmap_texture_id != 0 else 0
glBindTexture(GL_TEXTURE_2D, flow_tex_to_bind)
if flowMapLoc != -1:
glUniform1i(flowMapLoc, 1)
if timeLoc != -1: glUniform1f(timeLoc, self.anim_time)
if speedLoc != -1: glUniform1f(speedLoc, self.flow_speed)
if distLoc != -1: glUniform1f(distLoc, self.flow_distortion)
if baseScaleLoc != -1: glUniform1f(baseScaleLoc, float(getattr(self, 'base_scale', 1.0)))
if previewRepeatLoc != -1: glUniform1i(previewRepeatLoc, 1 if self.preview_repeat else 0)
if mainViewScaleLoc != -1: glUniform1f(mainViewScaleLoc, self.main_view_scale)
if mainViewOffsetLoc != -1: glUniform2f(mainViewOffsetLoc, self.main_view_offset.x(), self.main_view_offset.y())
if useDirectX != -1: glUniform1f(useDirectX, 1.0 if self.graphics_api_mode == "directx" else 0.0)
# 传递cover纵横比校正
loc_as = glGetUniformLocation(self.shader_program_id, "u_aspectScale")
if loc_as != -1:
glUniform2f(loc_as, float(self.aspect_scale_x), float(self.aspect_scale_y))
loc_ao = glGetUniformLocation(self.shader_program_id, "u_aspectOffset")
if loc_ao != -1:
glUniform2f(loc_ao, float(self.aspect_offset_x), float(self.aspect_offset_y))
glBindVertexArray(self.vao)
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
glBindVertexArray(0)
except GLError as e:
print(f"Error drawing main pass: {e}")
finally:
glActiveTexture(GL_TEXTURE1)
glBindTexture(GL_TEXTURE_2D, 0)
glActiveTexture(GL_TEXTURE0)
glBindTexture(GL_TEXTURE_2D, 0)
glUseProgram(0)
# Draw UV wire overlay (full-screen, over base/flowmap)
if getattr(self, 'uv_wire_enabled', False) and self.uv_wire_program != 0 and getattr(self, 'uv_wire_index_count', 0) > 0:
try:
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glViewport(0, 0, self.width(), self.height())
glUseProgram(self.uv_wire_program)
ms = glGetUniformLocation(self.uv_wire_program, "u_mainViewScale")
if ms != -1:
glUniform1f(ms, self.main_view_scale)
mo = glGetUniformLocation(self.uv_wire_program, "u_mainViewOffset")
if mo != -1:
glUniform2f(mo, self.main_view_offset.x(), self.main_view_offset.y())
# 传递cover纵横比校正
loc_as = glGetUniformLocation(self.uv_wire_program, "u_aspectScale")
if loc_as != -1:
glUniform2f(loc_as, float(self.aspect_scale_x), float(self.aspect_scale_y))
loc_ao = glGetUniformLocation(self.uv_wire_program, "u_aspectOffset")
if loc_ao != -1:
glUniform2f(loc_ao, float(self.aspect_offset_x), float(self.aspect_offset_y))
col = glGetUniformLocation(self.uv_wire_program, "u_color")
if col != -1:
glUniform3f(col, 0.95, 0.5, 0.1)
op = glGetUniformLocation(self.uv_wire_program, "u_opacity")
if op != -1:
glUniform1f(op, float(getattr(self, 'uv_wire_opacity', 0.7)))
glBindVertexArray(self.uv_wire_vao)
glLineWidth(float(getattr(self, 'uv_wire_line_width', 1.0)))
glDrawElements(GL_LINES, int(self.uv_wire_index_count), GL_UNSIGNED_INT, None)
glBindVertexArray(0)
except Exception as e:
print(f"Error drawing UV wire: {e}")
finally:
glUseProgram(0)
glDisable(GL_BLEND)
# draw overlay image (if any) over MAIN VIEW (full-screen), not the small preview
if self.has_overlay and self.overlay_texture_id != 0 and self.overlay_shader_program_id != 0:
try:
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glViewport(0, 0, self.width(), self.height())
glUseProgram(self.overlay_shader_program_id)
glActiveTexture(GL_TEXTURE0)
glBindTexture(GL_TEXTURE_2D, self.overlay_texture_id)
loc_tex = glGetUniformLocation(self.overlay_shader_program_id, "overlayMap")
if loc_tex != -1:
glUniform1i(loc_tex, 0)
loc_op = glGetUniformLocation(self.overlay_shader_program_id, "u_opacity")
if loc_op != -1:
glUniform1f(loc_op, float(self.overlay_opacity))
loc_ms = glGetUniformLocation(self.overlay_shader_program_id, "u_mainViewScale")
if loc_ms != -1:
glUniform1f(loc_ms, self.main_view_scale)
loc_mo = glGetUniformLocation(self.overlay_shader_program_id, "u_mainViewOffset")
if loc_mo != -1:
glUniform2f(loc_mo, self.main_view_offset.x(), self.main_view_offset.y())
loc_rep = glGetUniformLocation(self.overlay_shader_program_id, "u_repeat")
if loc_rep != -1:
glUniform1i(loc_rep, 1 if self.preview_repeat else 0)
# 传递cover纵横比校正
loc_as = glGetUniformLocation(self.overlay_shader_program_id, "u_aspectScale")
if loc_as != -1:
glUniform2f(loc_as, float(self.aspect_scale_x), float(self.aspect_scale_y))
loc_ao = glGetUniformLocation(self.overlay_shader_program_id, "u_aspectOffset")
if loc_ao != -1:
glUniform2f(loc_ao, float(self.aspect_offset_x), float(self.aspect_offset_y))
glBindVertexArray(self.vao)
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
glBindVertexArray(0)
except Exception as e:
print(f"Error drawing reference overlay: {e}")
finally:
glUseProgram(0)
glBindTexture(GL_TEXTURE_2D, 0)
glDisable(GL_BLEND)
# draw preview overlay (top-right small viewport) LAST
pv_w = int(self.preview_size.width() * self.width())
pv_h = int(self.preview_size.height() * self.height())
pv_x = int(self.preview_pos.x() * self.width())
pv_y = int((1.0 - self.preview_pos.y() - self.preview_size.height()) * self.height())
try:
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glViewport(pv_x, pv_y, pv_w, pv_h)
glUseProgram(self.preview_shader_program_id)
glActiveTexture(GL_TEXTURE0)
glBindTexture(GL_TEXTURE_2D, self.flowmap_texture_id if self.flowmap_texture_id != 0 else 0)
loc_flow = glGetUniformLocation(self.preview_shader_program_id, "flowMap")
if loc_flow != -1:
glUniform1i(loc_flow, 0)
loc_off = glGetUniformLocation(self.preview_shader_program_id, "u_previewOffset")
if loc_off != -1:
glUniform2f(loc_off, self.preview_offset.x(), self.preview_offset.y())
loc_rep = glGetUniformLocation(self.preview_shader_program_id, "u_previewRepeat")
if loc_rep != -1:
glUniform1i(loc_rep, 1 if self.preview_repeat else 0)
glBindVertexArray(self.vao)
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
glBindVertexArray(0)
except Exception as e:
print(f"Error drawing preview overlay: {e}")
finally:
glUseProgram(0)
glBindTexture(GL_TEXTURE_2D, 0)
glDisable(GL_BLEND)
def resizeGL(self, w, h):
"""处理窗口大小变化事件。
取消对widget高度的强制锁定,仅更新视口与内部纵横比校正,
让画布始终填满分配区域,避免出现外部空白条。"""
# 检查尺寸有效性
if w <= 0 or h <= 0:
return
# 保存当前窗口尺寸
self.window_width = w
self.window_height = h
# 设置OpenGL视口
glViewport(0, 0, w, h)
# 确保纹理宽高比存在
if not hasattr(self, 'texture_original_aspect_ratio') or self.texture_original_aspect_ratio <= 0:
if self.texture_size[0] > 0 and self.texture_size[1] > 0:
self.texture_original_aspect_ratio = self.texture_size[0] / self.texture_size[1]
else:
self.texture_original_aspect_ratio = 1.0
# 计算 cover 模式的纵横比校正(内容填满而不拉伸:多余部分裁剪)
self._update_cover_aspect()
# 更新 shader 用的比例校正
self.update_aspect_ratio()
self.update_preview_size()
self.resized.emit()
self.update()
def _update_cover_aspect(self):
"""计算屏幕到内容坐标的cover映射参数: Tex' = (Tex - offset)/scale
目标:内容等比填满widget,超出部分裁剪,不产生拉伸。"""
tex_w, tex_h = self.texture_size
if tex_w <= 0 or tex_h <= 0:
self.aspect_scale_x = self.aspect_scale_y = 1.0
self.aspect_offset_x = self.aspect_offset_y = 0.0
return
win_w = max(1, self.width())
win_h = max(1, self.height())
r_tex = float(tex_w) / float(tex_h)
r_win = float(win_w) / float(win_h)
if r_win > r_tex:
# widget 更宽:应左右裁剪,但我们在screen->content中需要把"裁剪轴"的scale放在Y以避免XY反置
self.aspect_scale_x = 1.0
self.aspect_scale_y = r_win / r_tex
self.aspect_offset_x = 0.0
self.aspect_offset_y = (1.0 - self.aspect_scale_y) * 0.5
else:
# widget 更高(更窄):应上下裁剪,对应将scale放在X
self.aspect_scale_x = r_tex / r_win
self.aspect_scale_y = 1.0
self.aspect_offset_x = (1.0 - self.aspect_scale_x) * 0.5
self.aspect_offset_y = 0.0
def wheelEvent(self, event):
"""处理鼠标滚轮事件,用于缩放主视图"""
# 主视图的缩放 - 以窗口中心为缩放点
# 调整缩放
delta = event.angleDelta().y()
new_scale = self.main_view_scale
if delta > 0:
# 放大,但限制最大缩放
new_scale = min(self.MAX_SCALE, self.main_view_scale * (1.0 + self.SCROLL_SENSITIVITY))
else:
# 缩小,但限制最小缩放
new_scale = max(self.MIN_SCALE, self.main_view_scale / (1.0 + self.SCROLL_SENSITIVITY))
# 计算窗口中心点并转换为场景坐标
center_widget = QPoint(self.width() // 2, self.height() // 2)
center_scene = self.mapToScene(center_widget)
# 缩放后的新场景坐标应该和缩放前的相同,计算所需的新偏移
old_offset = self.main_view_offset
# 正确的偏移量计算公式
new_offset = QPointF(
old_offset.x() + 0.5 * (1.0/new_scale - 1.0/self.main_view_scale),
old_offset.y() + 0.5 * (1.0/new_scale - 1.0/self.main_view_scale)
)
# 设置目标值,启用平滑过渡
self.target_main_view_scale = new_scale
self.target_main_view_offset = new_offset
self.scale_animation_active = True
self.scale_animation_start_time = time.time()
self.update() # 请求重绘
def keyPressEvent(self, event):
"""处理键盘事件"""
if event.key() == Qt.Key_Space or event.key() == Qt.Key_F:
# 空格键或F键重置视图到中心
self.target_main_view_scale = 1.0
self.target_main_view_offset = QPointF(0.0, 0.0)
self.scale_animation_active = True
self.scale_animation_start_time = time.time()
self.update()
elif event.key() == Qt.Key_Shift:
# 按下Shift键时激活模糊模式
self.shift_pressed = True
elif event.key() == Qt.Key_S:
# 按下S键,标记为笔刷调整模式
self.s_pressed = True
# 始终使用当前鼠标位置作为S键按下时的位置
cursor_pos = self.mapFromGlobal(QCursor.pos())
# 确保位置在widget内
if self.rect().contains(cursor_pos):
self.s_press_position = cursor_pos
else:
# 如果鼠标不在widget内,使用widget中心作为默认位置
self.s_press_position = QPoint(self.width() // 2, self.height() // 2)
# 保存当前笔刷参数作为初始值
self.initial_brush_radius = self.brush_radius
self.initial_brush_strength = self.brush_strength
# 发送鼠标位置信号以更新笔刷预览位置
self.mouse_moved.emit(self.s_press_position)
else:
super().keyPressEvent(event)
def keyReleaseEvent(self, event):
"""处理键盘释放事件"""
if event.key() == Qt.Key_Shift:
# 释放Shift键时关闭模糊模式
self.shift_pressed = False
elif event.key() == Qt.Key_S:
# 释放S键
self.s_pressed = False
self.s_press_position = None # 清除S键按下时的位置
else:
super().keyReleaseEvent(event)
def leaveEvent(self, event):
"""当鼠标离开画布时,强制退出S键调整模式,防止状态卡住"""
try:
self.s_pressed = False
self.s_press_position = None
# 离开时若正在绘制,结束绘制,防止漂移
if self.mouse_state == MouseState.DRAWING or self.mouse_state == MouseState.ERASING:
self.mouse_state = MouseState.IDLE
self.is_drawing = False
self.is_erasing = False
try:
self.drawingFinished.emit()
except Exception:
pass
# 发出离开信号用于隐藏笔刷UI
try:
self.hover_left.emit()
except Exception:
pass
except Exception:
pass
return super().leaveEvent(event)
def focusOutEvent(self, event):
"""当画布失去焦点时,强制退出S键调整模式,防止状态卡住"""
try:
self.s_pressed = False
self.s_press_position = None
except Exception:
pass
return super().focusOutEvent(event)
def enterEvent(self, event):
try:
self.hover_entered.emit()
except Exception:
pass
return super().enterEvent(event)
def mousePressEvent(self, event: QMouseEvent):
# 检查鼠标是否在预览区域
if self.is_in_preview(event.pos()):
if event.button() == Qt.MiddleButton:
# 开始拖动预览窗口视角
self.mouse_state = MouseState.DRAG_PREVIEW
self.is_dragging_preview = True
self.last_mouse_pos = event.pos()
return
else:
# 调试打印当前鼠标位置和映射后的场景坐标
if event.button() == Qt.LeftButton:
self.debug_coordinates(event.pos())
if event.button() == Qt.MiddleButton:
# 开始拖动主视图
self.mouse_state = MouseState.DRAG_MAIN
self.is_dragging_main_view = True
self.last_mouse_pos = event.pos()
return
if event.button() == Qt.LeftButton:
# 标记为鼠标输入
self.is_tablet_input = False
# 鼠标事件时重置为最大压力(速度影响会在绘制时动态调整)
self.current_pressure = 1.0
self.update_brush_from_pressure()
self.mouse_state = MouseState.DRAWING
self.is_drawing = True
self.is_erasing = False
self.last_pos = event.pos()
# 发出绘制开始信号
self.drawingStarted.emit()
self.apply_brush(event.pos(), event.pos())
self.update()
# 在鼠标按下时不发出flowmap_updated信号,只在释放时发出
elif event.button() == Qt.RightButton: # 新增右键擦除功能
# 标记为鼠标输入
self.is_tablet_input = False
# 鼠标事件时重置为最大压力
self.current_pressure = 1.0
self.update_brush_from_pressure()
self.mouse_state = MouseState.ERASING
self.is_drawing = True
self.is_erasing = True
self.last_pos = event.pos()
# 发出绘制开始信号
self.drawingStarted.emit()
self.apply_brush(event.pos(), event.pos())
self.update()
# 在鼠标按下时不发出flowmap_updated信号,只在释放时发出
def mouseMoveEvent(self, event: QMouseEvent):
# 处理S键+鼠标移动的快捷键功能 - 优先级最高
if self.s_pressed and self.s_press_position:
current_pos = event.pos()
# 计算与S键按下位置的差值
delta_x = current_pos.x() - self.s_press_position.x()
delta_y = current_pos.y() - self.s_press_position.y()
# 判断移动距离更大的方向,并只应用对应参数的变化
if abs(delta_x) > abs(delta_y):
# 水平移动更明显 - 只调整笔刷大小
scale_factor = 0.1 # 调整灵敏度系数
new_radius = self.initial_brush_radius + delta_x * scale_factor
new_radius = max(5.0, min(200.0, new_radius)) # 限制笔刷大小范围
# 同时更新当前值和基础值
self.brush_radius = new_radius
self.base_brush_radius = new_radius
# 保持强度不变
self.brush_strength = self.initial_brush_strength
self.base_brush_strength = self.initial_brush_strength
else:
# 垂直移动更明显 - 只调整流动强度
# 向上减小,向下增加,从初始值开始调整
scale_factor = 0.005 # 调整灵敏度系数
new_strength = self.initial_brush_strength - delta_y * scale_factor
new_strength = max(0.01, min(1.0, new_strength)) # 限制强度范围
# 同时更新当前值和基础值
self.brush_strength = new_strength
self.base_brush_strength = new_strength
# 保持半径不变
self.brush_radius = self.initial_brush_radius
self.base_brush_radius = self.initial_brush_radius
# 发出笔刷属性变化信号
self.brush_properties_changed.emit(self.brush_radius, self.brush_strength)
# 无论调整哪个参数,发送鼠标位置信号保持笔刷预览在原位
self.mouse_moved.emit(self.s_press_position) # 使用 S 按下时的位置而不是 last_pos
self.update()
return
# 使用状态枚举处理不同的鼠标状态
if self.mouse_state == MouseState.DRAG_PREVIEW:
current_pos = event.pos()
delta_x = current_pos.x() - self.last_mouse_pos.x()
delta_y = current_pos.y() - self.last_mouse_pos.y()
# 转换为预览窗口的归一化坐标
dx = delta_x / (self.width() * self.preview_size.width())
dy = delta_y / (self.height() * self.preview_size.height())
# 调整偏移量 - 预览窗口在右下角
self.preview_offset += QPointF(dx, dy)
self.last_mouse_pos = current_pos
self.update()
return
elif self.mouse_state == MouseState.DRAG_MAIN:
current_pos = event.pos()
delta_x = current_pos.x() - self.last_mouse_pos.x()
delta_y = current_pos.y() - self.last_mouse_pos.y()
# 转换为归一化坐标的偏移量
dx = delta_x / self.width()
dy = delta_y / self.height()
# 根据当前缩放调整偏移量
self.main_view_offset += QPointF(dx / self.main_view_scale,
-dy / self.main_view_scale) # 注意这里修改了符号,使纵向拖拽方向正确
self.last_mouse_pos = current_pos
self.update() # 请求重绘
return
# 发送鼠标移动信号,用于更新画笔预览
if not self.s_pressed:
pos = QPoint(event.pos().x(), event.pos().y())
self.mouse_moved.emit(pos)
# 始终将鼠标位置传递给预览
scene_pos = self.mapToScene(event.pos())
self.mouseMoveNonDrawing.emit(scene_pos)
# 如果正在绘制且没有按下S键,则应用笔刷
if not self.s_pressed and (self.mouse_state == MouseState.DRAWING or self.mouse_state == MouseState.ERASING) and \
((event.buttons() & Qt.LeftButton) or (event.buttons() & Qt.RightButton)):
current_pos = event.pos()
# 绘制节流控制 - 限制绘制频率以提高性能
current_time = time.time() * 1000 # 转换为毫秒
time_since_last_draw = current_time - self.last_draw_time
if time_since_last_draw >= self.draw_throttle_ms:
# 时间间隔足够,执行绘制
# 使用插值算法填充跳过的点,避免快速绘制时出现断点
self.apply_brush_with_interpolation(self.last_pos, current_pos)
self.last_pos = current_pos
self.last_draw_time = current_time
# 如果有累积的位置,一并处理掉
if self.accumulated_positions:
self.accumulated_positions = []
# 更新屏幕
self.update()
# 在鼠标移动过程中不发送flowmap_updated信号,减少信号数量
self.update_pending = False
else:
# 累积鼠标位置,等待下次绘制
self.accumulated_positions.append(current_pos)
# 如果还没有安排更新,则安排一个
if not self.update_pending:
self.update_pending = True
# 计算剩余等待时间
remaining_time = max(1, int(self.draw_throttle_ms - time_since_last_draw))
# 使用 singleShot 计时器在适当时间后触发更新
QTimer.singleShot(remaining_time, self.process_accumulated_positions)
def process_accumulated_positions(self):
"""处理在节流期间累积的鼠标位置"""
if not self.accumulated_positions or (self.mouse_state != MouseState.DRAWING and self.mouse_state != MouseState.ERASING):
self.update_pending = False
return
# 取最近的点与上次绘制点之间绘制一条线
current_pos = self.accumulated_positions[-1]
# 执行绘制
self.apply_brush_with_interpolation(self.last_pos, current_pos)
self.last_pos = current_pos
self.last_draw_time = time.time() * 1000
# 清除累积的位置
self.accumulated_positions = []