Skip to content

Commit 75322c1

Browse files
authored
add meme sphere_rotate (#275)
1 parent d96845e commit 75322c1

File tree

1 file changed

+166
-0
lines changed

1 file changed

+166
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import math
2+
import struct
3+
from datetime import datetime
4+
5+
import skia
6+
from pil_utils import BuildImage
7+
8+
from meme_generator import add_meme
9+
from meme_generator.utils import (
10+
Maker,
11+
from_skia_image,
12+
make_gif_or_combined_gif,
13+
new_skia_surface,
14+
skia_sampling_options,
15+
to_skia_image,
16+
)
17+
18+
19+
def sphere_rotate(images: list[BuildImage], texts, args):
20+
total_frames = 60
21+
22+
sksl_code = """
23+
uniform shader image;
24+
uniform float angle;
25+
uniform float2 canvas_size;
26+
uniform float2 image_size;
27+
28+
const float PI = 3.14159265359;
29+
30+
// Y轴旋转矩阵
31+
mat3 rotate_y(float a) {
32+
float s = sin(a);
33+
float c = cos(a);
34+
return mat3(
35+
c, 0, s,
36+
0, 1, 0,
37+
-s, 0, c
38+
);
39+
}
40+
41+
// 光线与球体相交检测
42+
// ro: 光线原点, rd: 光线方向, r: 球体半径
43+
// 返回 vec2(近交点距离, 远交点距离), 如果不相交则都为-1.0
44+
vec2 intersect_sphere(vec3 ro, vec3 rd, float r) {
45+
float b = dot(ro, rd);
46+
float c = dot(ro, ro) - r * r;
47+
float h = b * b - c;
48+
if (h < 0.0) {
49+
return vec2(-1.0);
50+
}
51+
float sqrt_h = sqrt(h);
52+
return vec2(-b - sqrt_h, -b + sqrt_h);
53+
}
54+
55+
// 将球体表面的3D坐标点转换为2D纹理UV坐标 (等距柱状投影)
56+
vec2 get_sphere_uv(vec3 p) {
57+
p = normalize(p);
58+
float u = 0.5 + atan(p.z, p.x) / (2.0 * PI);
59+
float v = 0.5 + asin(p.y) / PI;
60+
return vec2(u, v);
61+
}
62+
63+
half4 main(vec2 coord) {
64+
// 将屏幕像素坐标转换为标准化坐标
65+
vec2 uv = (2.0 * coord - canvas_size.xy) / canvas_size.y;
66+
67+
// 设置相机 (光线追踪)
68+
vec3 ro = vec3(0.0, 0.0, 3.5);
69+
vec3 rd = normalize(vec3(uv, -2.0));
70+
71+
// 应用Y轴旋转
72+
mat3 rot = rotate_y(angle);
73+
ro = rot * ro;
74+
rd = rot * rd;
75+
76+
float radius = 1.2;
77+
78+
// 寻找光线与完整球体的所有交点
79+
vec2 t = intersect_sphere(ro, rd, radius);
80+
float t_hit = -1.0;
81+
82+
// 测试近处的交点
83+
if (t.x > 0.0) {
84+
vec3 p1 = ro + rd * t.x;
85+
if (p1.x >= 0.0) {
86+
t_hit = t.x;
87+
}
88+
}
89+
90+
// 如果近处交点无效,则测试远处的交点 (处理看到内壁的情况)
91+
if (t_hit < 0.0 && t.y > 0.0) {
92+
vec3 p2 = ro + rd * t.y;
93+
if (p2.x >= 0.0) {
94+
t_hit = t.y;
95+
}
96+
}
97+
98+
// 如果找到了有效的交点,进行着色
99+
if (t_hit > 0.0) {
100+
vec3 pos = ro + rd * t_hit;
101+
vec3 normal = normalize(pos);
102+
float facing = dot(rd, normal);
103+
104+
vec2 tex_uv = get_sphere_uv(pos);
105+
vec2 tex_coords = tex_uv * image_size;
106+
half4 tex_color = image.eval(tex_coords);
107+
108+
if (facing < 0.0) {
109+
// 外表面: 直接使用纹理颜色
110+
return tex_color;
111+
} else {
112+
// 内表面: 将纹理颜色变暗
113+
return tex_color * half4(0.7, 0.7, 0.7, 1.0);
114+
}
115+
}
116+
117+
return half4(0.0);
118+
}
119+
"""
120+
effect = skia.RuntimeEffect.MakeForShader(sksl_code)
121+
122+
def maker(i: int) -> Maker:
123+
def make(imgs: list[BuildImage]):
124+
img = imgs[0].convert("RGBA")
125+
canvas_w, canvas_h = (300, 300)
126+
surface = new_skia_surface((canvas_w, canvas_h))
127+
canvas = surface.getCanvas()
128+
129+
angle = i / total_frames * math.pi * 2
130+
131+
values = []
132+
for uniform in effect.uniforms():
133+
if uniform.name == "angle":
134+
values.append(struct.pack("<f", angle))
135+
elif uniform.name == "canvas_size":
136+
values.append(struct.pack("<f", canvas_w))
137+
values.append(struct.pack("<f", canvas_h))
138+
elif uniform.name == "image_size":
139+
values.append(struct.pack("<f", img.width))
140+
values.append(struct.pack("<f", img.height))
141+
uniforms = skia.Data.MakeWithCopy(b"".join(values))
142+
143+
skia_image = to_skia_image(img.image)
144+
image_shader = skia_image.makeShader(skia_sampling_options())
145+
shader = effect.makeShader(uniforms, image_shader, 1)
146+
147+
paint = skia.Paint()
148+
paint.setShader(shader)
149+
canvas.drawPaint(paint)
150+
frame = BuildImage(from_skia_image(surface.makeImageSnapshot()))
151+
return frame
152+
153+
return make
154+
155+
return make_gif_or_combined_gif(images, maker, total_frames, 0.04)
156+
157+
158+
add_meme(
159+
"sphere_rotate",
160+
sphere_rotate,
161+
min_images=1,
162+
max_images=1,
163+
keywords=["球面旋转"],
164+
date_created=datetime(2025, 7, 6),
165+
date_modified=datetime(2025, 7, 6),
166+
)

0 commit comments

Comments
 (0)