|
| 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