Skip to content

Commit 939a774

Browse files
committed
updated to use new widgit
1 parent 2b40220 commit 939a774

File tree

3 files changed

+180
-100
lines changed

3 files changed

+180
-100
lines changed

Cube/Qt.py

Lines changed: 0 additions & 75 deletions
This file was deleted.

Cube/WebGPU.py

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
from WebGPUWidget import WebGPUWidget
12
import nccapy
23
import numpy as np
34
import wgpu
45
import wgpu.backends.auto
56

67

7-
class WebGPU:
8+
class WebGPU(WebGPUWidget):
89
def __init__(self):
10+
super().__init__()
11+
12+
def initializeWebGPU(self):
913
self.init_context()
1014
self.load_shaders("vertex_shader.wgsl", "fragment_shader.wgsl")
1115
self.create_geo()
@@ -45,18 +49,18 @@ def load_shaders(self, vertex_shader, fragment_shader):
4549
self.fragment_shader = self.device.create_shader_module(code=fragment_shader_code)
4650

4751
def create_geo(self):
48-
# fmt: off
4952
# Cube vertex data
50-
vertices = np.array([
51-
# Positions # Colors
52-
-1, -1, -1, 1, 0, 0,
53-
1, -1, -1, 0, 1, 0,
54-
1, 1, -1, 0, 0, 1,
55-
-1, 1, -1, 1, 1, 0,
56-
-1, -1, 1, 1, 0, 1,
57-
1, -1, 1, 0, 1, 1,
58-
1, 1, 1, 1, 1, 1,
59-
-1, 1, 1, 0, 0, 0,
53+
# fmt: off
54+
vertices = np.array([
55+
# Positions # Colors
56+
-1, -1, -1, 1, 0, 0,
57+
1, -1, -1, 0, 1, 0,
58+
1, 1, -1, 0, 0, 1,
59+
-1, 1, -1, 1, 1, 0,
60+
-1, -1, 1, 1, 0, 1,
61+
1, -1, 1, 0, 1, 1,
62+
1, 1, 1, 1, 1, 1,
63+
-1, 1, 1, 0, 0, 0,
6064
], dtype=np.float32)
6165

6266
self.indices = np.array([
@@ -67,10 +71,11 @@ def create_geo(self):
6771
0, 3, 7, 7, 4, 0,
6872
1, 2, 6, 6, 5, 1,
6973
], dtype=np.uint16)
74+
# fmt: on
7075
# Create the vertex buffer
7176
self.vertex_buffer = self.device.create_buffer_with_data(
72-
data=vertices, usage=wgpu.BufferUsage.VERTEX
73-
)
77+
data=vertices, usage=wgpu.BufferUsage.VERTEX
78+
)
7479

7580
# Create the index buffer
7681
self.index_buffer = self.device.create_buffer_with_data(
@@ -139,7 +144,7 @@ def create_pipeline(self):
139144
multisample={"count": 1, "mask": 0xFFFFFFFF, "alpha_to_coverage_enabled": False},
140145
)
141146

142-
def get_colour_buffer(self):
147+
def _update_colour_buffer(self):
143148
buffer_size = (
144149
1024 * 720 * 4
145150
) # Width * Height * Bytes per pixel (RGBA8 is 4 bytes per pixel)
@@ -162,29 +167,26 @@ def get_colour_buffer(self):
162167

163168
# Access the mapped memory
164169
raw_data = readback_buffer.read_mapped()
165-
pixel_data = np.frombuffer(raw_data, dtype=np.uint8).reshape(
170+
self.buffer = np.frombuffer(raw_data, dtype=np.uint8).reshape(
166171
(1024, 720, 4)
167172
) # Height, Width, Channels
168173

169174
# Unmap the buffer when done
170175
readback_buffer.unmap()
171-
return pixel_data
172176

173177
def update_uniform_buffers(self):
174178
self.rotation += 1
175179
x = nccapy.Mat4.rotate_x(self.rotation)
176180
y = nccapy.Mat4.rotate_y(self.rotation)
177181
z = nccapy.Mat4.rotate_z(self.rotation)
178182
rotation = x @ y @ z
179-
self.mvp_matrix = mvp_matrixncca = (
180-
(self.persp @ self.lookat @ rotation).get_numpy().astype(np.float32)
181-
)
183+
self.mvp_matrix = (self.persp @ self.lookat @ rotation).get_numpy().astype(np.float32)
182184

183185
self.device.queue.write_buffer(
184186
buffer=self.uniform_buffer, buffer_offset=0, data=self.mvp_matrix.tobytes()
185187
)
186188

187-
def render(self):
189+
def paintWebGPU(self):
188190
command_encoder = self.device.create_command_encoder()
189191
render_pass = command_encoder.begin_render_pass(
190192
label="render_pass",
@@ -211,22 +213,23 @@ def render(self):
211213
render_pass.set_index_buffer(self.index_buffer, wgpu.IndexFormat.uint16)
212214
render_pass.draw_indexed(len(self.indices), 1, 0, 0, 0)
213215
render_pass.end()
214-
215216
# Submit the commands
216217
self.device.queue.submit([command_encoder.finish()])
218+
self._update_colour_buffer()
217219

218220
def create_uniform_buffers(self):
219221
self.persp = nccapy.perspective(45.0, 1.0, 0.1, 100.0)
220222
self.lookat = nccapy.look_at(
221223
nccapy.Vec3(0, 0, 5), nccapy.Vec3(0, 0, 0), nccapy.Vec3(0, 1, 0)
222224
)
223225
rotation = nccapy.Mat4.rotate_y(40)
224-
self.mvp_matrix = mvp_matrixncca = (
225-
(self.persp @ self.lookat @ rotation).get_numpy().astype(np.float32)
226-
)
226+
self.mvp_matrix = (self.persp @ self.lookat @ rotation).get_numpy().astype(np.float32)
227227

228228
self.uniform_buffer = self.device.create_buffer_with_data(
229229
data=self.mvp_matrix.astype(np.float32),
230230
usage=wgpu.BufferUsage.UNIFORM | wgpu.BufferUsage.COPY_DST,
231231
label="uniform_buffer MVP",
232232
)
233+
234+
def resizeWebGPU(self, width, height):
235+
pass

Cube/WebGPUWidget.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
from abc import ABCMeta, abstractmethod
2+
from typing import List, Tuple
3+
4+
import numpy as np
5+
from qtpy.QtCore import QRect, Qt
6+
from qtpy.QtGui import QColor, QFont, QImage, QPainter
7+
from qtpy.QtWidgets import QWidget
8+
9+
10+
class QWidgetABCMeta(type(QWidget), ABCMeta):
11+
"""
12+
A metaclass that combines the functionality of ABCMeta and QWidget's metaclass.
13+
14+
This allows the creation of abstract base classes that are also QWidgets.
15+
"""
16+
17+
pass
18+
19+
20+
class WebGPUWidget(QWidget, metaclass=QWidgetABCMeta):
21+
"""
22+
An abstract base class for WebGPU widgets.
23+
24+
This class provides a template for creating WebGPU widgets with methods
25+
that must be implemented in subclasses. It is designed to be similar to the QOpenGLWidget class.
26+
27+
Attributes:
28+
initialized (bool): A flag indicating whether the widget has been initialized, default is False and will allow initializeWebGPU to be called once.
29+
"""
30+
31+
def __init__(self) -> None:
32+
"""
33+
Initialize the AbstractWebGPUWidget.
34+
35+
This constructor initializes the QWidget and sets the initialized flag to False.
36+
"""
37+
super().__init__()
38+
self.initialized = False
39+
self.text_buffer: List[Tuple[int, int, str, int, str, QColor]] = []
40+
self.buffer = None
41+
42+
@abstractmethod
43+
def initializeWebGPU(self) -> None:
44+
"""
45+
Initialize the WebGPU context.
46+
47+
This method must be implemented in subclasses to set up the WebGPU context. Will be called once.
48+
"""
49+
pass
50+
51+
@abstractmethod
52+
def paintWebGPU(self) -> None:
53+
"""
54+
Paint the WebGPU content.
55+
56+
This method must be implemented in subclasses to render the WebGPU content. This will be called on every paint event
57+
and is where all the main rendering code should be placed.
58+
"""
59+
pass
60+
61+
@abstractmethod
62+
def resizeWebGPU(self, width: int, height: int) -> None:
63+
"""
64+
Resize the WebGPU context.
65+
66+
This method must be implemented in subclasses to handle resizing of the WebGPU context. Will be called on a resize of the widget.
67+
68+
Args:
69+
width (int): The new width of the widget.
70+
height (int): The new height of the widget.
71+
"""
72+
pass
73+
74+
def paintEvent(self, event) -> None:
75+
"""
76+
Handle the paint event to render the WebGPU content.
77+
78+
Args:
79+
event (QPaintEvent): The paint event.
80+
"""
81+
if not self.initialized:
82+
self.initializeWebGPU()
83+
self.initialized = True
84+
self.paintWebGPU()
85+
painter = QPainter(self)
86+
87+
if self.buffer is not None:
88+
self._present_image(painter, self.buffer)
89+
for x, y, text, size, font, colour in self.text_buffer:
90+
painter.setPen(colour)
91+
painter.setFont(QFont("Arial", size))
92+
painter.drawText(x, y, text)
93+
self.text_buffer.clear()
94+
95+
return super().paintEvent(event)
96+
97+
def render_text(
98+
self,
99+
x: int,
100+
y: int,
101+
text: str,
102+
size: int = 10,
103+
font: str = "Arial",
104+
colour: QColor = Qt.black,
105+
) -> None:
106+
"""
107+
Add text to the buffer to be rendered on the canvas.
108+
109+
Args:
110+
x (int): The x-coordinate of the text.
111+
y (int): The y-coordinate of the text.
112+
text (str): The text to render.
113+
size (int, optional): The font size of the text. Defaults to 10.
114+
font (str, optional): The font family of the text. Defaults to "Arial".
115+
colour (QColor, optional): The colour of the text. Defaults to Qt.black.
116+
"""
117+
self.text_buffer.append((x, y, text, size, font, colour))
118+
119+
def resizeEvent(self, event) -> None:
120+
"""
121+
Handle the resize event to adjust the WebGPU context.
122+
123+
Args:
124+
event (QResizeEvent): The resize event.
125+
"""
126+
self.resizeWebGPU(event.size().width(), event.size().height())
127+
return super().resizeEvent(event)
128+
129+
def _present_image(self, painter, image_data: np.ndarray) -> None:
130+
"""
131+
Present the image data on the canvas.
132+
133+
Args:
134+
image_data (np.ndarray): The image data to render.
135+
"""
136+
size = image_data.shape[0], image_data.shape[1] # width, height
137+
# We want to simply blit the image (copy pixels one-to-one on framebuffer).
138+
# Maybe Qt does this when the sizes match exactly (like they do here).
139+
# Converting to a QPixmap and painting that only makes it slower.
140+
141+
# Just in case, set render hints that may hurt performance.
142+
painter.setRenderHints(
143+
painter.RenderHint.Antialiasing | painter.RenderHint.SmoothPixmapTransform, False
144+
)
145+
146+
image = QImage(
147+
image_data.flatten(), size[0], size[1], size[0] * 4, QImage.Format.Format_RGBA8888
148+
)
149+
150+
rect1 = QRect(0, 0, size[0], size[1])
151+
rect2 = self.rect()
152+
painter.drawImage(rect2, image, rect1)

0 commit comments

Comments
 (0)