1. Fragment Shader
fragment 着色器需要返回vec4f的向量作为颜色,-> @location(0) vec4<f32>表示返回的颜色存储在@location(0)的公共位置上
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4f(1.0, 1.0, 0.0, 1.0);
}
结果
2. Vec
向量支持相加,相加时同位置分量相加
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var blueChannel = vec4f(0.0, 0.0, 0.75, 0.0);
var redChannel = vec4f(0.5, 0.0, 0.0, 0.0);
var alphaChannel = vec4f(0.0, 0.0, 0.0, 1.0);
return blueChannel + redChannel + alphaChannel;
}
结果
3. Swizzling
向量可以通过(x,y,z,w)或(r,g,b,a)或(s,t,p,q)访问,不同顺序最后展示值相应变化
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var blueChannel = vec4f(0.0, 0.0, 0.75, 0.0);
var redChannel = vec4f(0.5, 0.0, 0.0, 0.0);
var alphaChannel = vec4f(0.0, 0.0, 0.0, 1.0);
var color = blueChannel + redChannel + alphaChannel;
return color.gbra;
}
结果
4. UV Coordinates & Uniforms
通过Bevy shader导入VertexOutput中获取uv,原始值与预期有diff,通过1.0 - uv.x将x值取反,反馈到结果是水平翻转,返回取uv.yx将xy值反转,返回到结果为沿/方向翻转
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
uv.x = 1.0 - uv.x;
return vec4f(uv.yx, 0.0, 1.0);
}
结果
5. Step
通过内置step函数第二个参数小于第一个参数返回0.0,大于第一个参数返回1.0,所以左半屏为黑uv.x<0.5,右半屏为红uv.x>0.5
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
return vec4f(step(0.5, uv.x), 0.0, 0.0, 1.0);
}
结果
6. Step - Invert
因为分量都是0-1直接浮点数,所以可以通过1.0 - *对分量取反
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
return vec4f(1.0 - step(0.5, uv.x), 0.0, 0.0, 1.0);
}
结果
7. Max
通过step函数判断像素区间,t1表示像素是否小于屏幕1/4,小于时t1值为1.0否则为0.0,t2表示像素是否大于屏幕3/4,大于时t1值为1.0否则为0.0,再通过max函数设置颜色,max函数返回两个参数中较大值,所以在小于屏幕1/4和大于屏幕3/4位置被上色了
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var color = vec3f(1.0, 0.3, 0.3);
var t1 = 1.0 - step(0.25, uv.x);
var t2 = step(0.75, uv.x);
return vec4f(color * max(t1, t2), 1.0);
}
结果
8. Step Union
通过Bevy shader导入View并将其存入@group(0) @binding(0) var<uniform> view0组0位uniform类型,View中viewport为vec4f向量,zw表示宽高,通过50.0 / view.viewport.z获得50像素占屏幕宽度的比例,
通过50.0 / view.viewport.w获得50像素占屏幕高度的比例,t1表示像素是否距离屏幕左侧50像素内,t2表示像素是否距离屏幕右侧50像素内,t3表示像素是否距离屏幕上方50像素内,t4表示像素是否距离屏幕下方50像素内,三次max函数取值为判断像素是否在屏幕四周50像素的边框内,拆解开来开,max(t3, t4)判断像素是否在上方或者下方50像素内,max(t2, max(t3, t4))判断像素是否在像素是否距离屏幕右侧50像素内且上方或者下方50像素内,max(t1, max(t2, max(t3, t4)))判断像素是否在像素是否距离屏幕左侧或右侧50像素内且上方或者下方50像素内,后续再进行预期的处理同#4
#import bevy_render::view::View
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(0) @binding(0) var<uniform> view: View;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var xw = 50.0 / view.viewport.z;
var yh = 50.0 / view.viewport.w;
var t1 = step(1.0 - xw, uv.x);
var t2 = 1.0 - step(xw, uv.x);
var t3 = step(1.0 - yh, uv.y);
var t4 = 1.0 - step(yh, uv.y);
var t = max(t1, max(t2, max(t3, t4)));
uv.x = 1.0 - uv.x;
return vec4(uv.yx * t, 0.0, 1.0);
}
结果
9. Fract
fract函数用于返回浮点值的小数部分,该函数接受单个参数,可以是float、vec2、vec3或vec4,并返回一个介于0.0和1.0之间的值,将像素x值放大10倍后取小数部分,判断小数部分是否大于0.5,即可知道像素在左半部分还是右半部分,例:
- 0.153 * 10 -> 1.53 -> fract() -> 0.53 -> 0.53 > 0.5 需要着色
- 0.314 * 10 -> 3.14 -> fract() -> 0.14 -> 0.14 < 0.5 不需要着色
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var x = fract(uv.x * 10.0);
return vec4f(step(0.5, x), 0.0, 0.0, 1.0);
}
结果
10. Tile Pattern
通过操作UV坐标并使用fract函数,可以在屏幕上创建重复模式
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
fn pattern(uv: vec2f) -> f32 {
let new_uv = uv * 2.0 - 1.0;
var t = pow(new_uv.x * new_uv.x, 0.3) + pow(new_uv.y * new_uv.y, 0.3) - 1.0;
return step(0.0, t) * t * 10.0 + step(0.2, t);
}
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var columns = 5.0;
var rows = 3.0;
var repeated_uv = fract(uv * vec2(columns, rows));
return vec4f(pattern(repeated_uv), 0.0, 0.0, 1.0);
}
结果
11. Fract - Grid
cell获取像素位置与网格比例,step(margin / (cellSize + margin), fract(cell.x))取比例x分量小数并与间隔和网格比例对比,从而判断像素x分量是否在边框内,step(margin / (cellSize + margin), fract(cell.y))取比例y分量小数并与间隔和网格比例对比,从而判断像素y分量是否在边框内,两值相乘代表任意值在边框内输出则为0.0
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var cellSize = 50.0;
var margin = 10.0;
var cell = in.position.xy / (cellSize + margin);
var t = step(margin / (cellSize + margin), fract(cell.x));
t *= step(margin / (cellSize + margin), fract(cell.y));
return vec4f(t, 0.0, 0.0, 1.0);
}
结果
12. Mod
先将uv放大9倍将屏幕变为9份,通过step(0.5, x)设置右半部分上色,放大后的uv通过(uv.x + 1.0) % 3.0取模结果只在[0.0, 3.0]区间,[0.0, 1.0]区间则为每隔2列第三列原本红色的部分,step(1.0, (uv.x + 1.0) % 3.0)将结果转为0.0再与原来值相乘原来的x为1.0,相乘后为0.0,每隔2列第三列原本红色也被去色
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
uv *= 9.0;
var x = fract(uv.x);
x = step(0.5, x);
x *= step(1.0, (uv.x + 1.0) % 3.0);
return vec4f(x, 0.0, 0.0, 1.0);
}
结果
13. Mix
step(0.25, uv.x)判断像素是否在屏幕1/4内,通过* (uv.x -0.25)在屏幕1/4内容时t为0.0插值不变化,像素颜色保持color1,其他时候因子为[0.25, 1.0],使像素从1/4屏幕开始通过mix函数从color1向color2平滑变化
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var color2 = vec3(0.38, 0.12, 0.93);
var color1 = vec3(1.00, 0.30, 0.30);
var t = step(0.25, uv.x) * (uv.x - 0.25);
return vec4f(mix(color1, color2, t), 1.0);
}
结果
14. Mix - Bilinear Interpolation
r1根据像素x分量从红过渡到黑,r2根据像素x分量取反从蓝过渡到绿,color根据像素y分量取反从r1过渡到r2,为了最终显示与Task相同,r2和color加了取反操作
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var red = vec3f(1.0, 0.0, 0.0);
var black = vec3f(0.0, 0.0, 0.0);
var blue = vec3f(0.0, 0.0, 1.0);
var green = vec3f(0.0, 1.0, 0.0);
let r1 = mix(red, black, uv.x);
let r2 = mix(blue, green, 1.0 - uv.x);
let color = mix(r1, r2, 1.0 - uv.y);
return vec4f(color, 1.0);
}
结果
15. Smoothstep
smoothstep函数用于基于第三个插值因子在两个值之间进行平滑插值,该函数通过对插值过程应用平滑曲线,提供了与线性插值相比更平滑的过渡
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var red = vec3f(1.0, 0.0, 0.0);
var green = vec3f(0.0, 1.0, 0.0);
let color = mix(red, green, smoothstep(0.25, 0.75, uv.x));
return vec4f(color, 1.0);
}
结果
16. Abs
通过uv.x * 2.0 - 1.0计算像素x分量负值在屏幕左侧,像素x分量正值在屏幕右侧,通过abs函数取分量绝对值从而上色
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
return vec4f(abs(uv.x * 2.0 - 1.0), 0.0, 0.0, 1.0);
}
结果
17. ABS - Rhomb
通过提示菱形边为Y = K * X + B,uv -= 0.5将屏幕四分,1.0 - step(0.5, abs(uv.x) + abs(uv.y)像素x分量绝对值加像素y分量绝对值小于0.5时上色
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
uv -= 0.5;
return vec4f(1.0 - step(0.5, abs(uv.x) + abs(uv.y)), 0.0, 0.0, 1.0);
}
结果
18. Ceil
uv先放大10倍后通过ceil函数四舍五入获得整数,再将整数/10缩小获得屏幕每格色值,最终ceil后值-1第一格才会不上色
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
return vec4f((ceil(uv.x) - 1.0) / 10.0, 0.0, 0.0, 1.0);
}
结果
19. Int / Floor
定义数组长度常量COLORS_LENGTH,定义颜色数组colors并使用COLORS_LENGTH定义长度,将uv放大4倍,因为5位数组下标为[0, 4],使用floor函数取放大后x分量整数部分,所以渐变的开始颜色为当前floor后的结果,结束为下一个下标颜色,渐变因子就是放大后x分量小数部分,通过fract函数获取
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
const COLORS_LENGTH: u32 = 5;
const colors: array<vec3f, COLORS_LENGTH> = array<vec3f, COLORS_LENGTH>(
vec3f(1.0, 0.0, 0.0),
vec3f(0.0, 1.0, 0.0),
vec3f(0.0, 0.0, 1.0),
vec3f(1.0, 1.0, 0.0),
vec3f(0.0, 0.0, 0.0)
);
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
let x = uv.x * (f32(COLORS_LENGTH) - 1.0);
let index = floor(x);
let nextIndex = min(index + 1.0, f32(COLORS_LENGTH - 1)); // 避免数组越界
return vec4f(mix(colors[u32(index)], colors[u32(nextIndex)], fract(x)), 1.0);
}
结果
20. Distance / Length
通过distance函数获得当前像素到中心点距离,step(0.25, dist)距离小于0.25时返回0.0,再通过1.0 -进行取反
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var dist = distance(uv, vec2(0.5));
var t = 1.0 - step(0.25, dist);
return vec4f(t, 0.0, 0.0, 1.0);
}
结果
21. Aspect Ratio
通过view.viewport.z / view.viewport.w获得分辨率比例,在distance函数中,两个参数同时乘ratio保证最终效果为屏幕中心0.25范围根据分辨率比例修改的正圆
#import bevy_render::view::View
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(0) @binding(0) var<uniform> view: View;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var ratio = vec2f(view.viewport.z / view.viewport.w, 1.0);
var dist = distance(uv * ratio, vec2(0.5) * ratio);
var t = 1.0 - step(0.25, dist);
return vec4f(t, 0.0, 0.0, 1.0);
}
结果
22. Atan
通过atan2函数并/ π计算像素到中心点角度,在mix函数渐变中通过abs取角度的绝对值作为渐变因子,最后color -= step(0.5, dist);如果像素到中心点距离大于0.5则不上色
#import bevy_render::view::View
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(0) @binding(0) var<uniform> view: View;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var center = vec2f(0.5);
var ratio = vec2f(view.viewport.z / view.viewport.w, 1.0);
uv *= ratio;
center *= ratio;
var dist = distance(uv, center);
var dir = uv - center;
var angle = atan2(dir.y, dir.x) / 3.1415926;
var color1 = vec3f(1.0, 0.0, 0.0);
var color2 = vec3f(0.0, 1.0, 0.0);
var color = mix(color1, color2, abs(angle));
color -= step(0.5, dist);
return vec4f(color, 1.0);
}
结果
23. Sin
dist * 3.14 * 2.0画正弦波* 5.0重复5次,sin取输入弧度的正弦值
#import bevy_render::view::View
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(0) @binding(0) var<uniform> view: View;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var ratio = vec2f(view.viewport.z / view.viewport.w, 1.0);
var center = vec2f(0.5);
var dist = distance(uv * ratio, center * ratio);
var red = sin(dist * 3.14 * 2.0 * 5.0) * 0.5 + 0.5;
return vec4f(red, 0.0, 0.0, 1.0);
}
结果
24. Vector Normaliztion
normalize(uv - vec2f(0.25, 0.5))取像素到左半屏中心点归一化向量,像素超过0.5时使用normalize(uv - vec2f(0.75, 0.5))取像素到右半屏中心点归一化向量
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var normal = normalize(uv - vec2f(0.25, 0.5));
var t = normal.x;
if (uv.x > 0.5) {
normal = normalize(uv - vec2f(0.75, 0.5));
t = normal.y;
}
return vec4f(t, t, t, 1.0);
}
结果
25. Dot
通过点积画三角
#import bevy_render::view::View
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(0) @binding(0) var<uniform> view: View;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var ratio = vec2(view.viewport.z / view.viewport.w, 1.0);
var p0 = vec2f(0.5, 0.75) * ratio; // 顶点
var p1 = vec2f(0.5, 0.0) * ratio; // 向底的射线
var p2 = uv * ratio; // 当前点
var dir1 = p1 - p0; // 射线到顶点向量差
var dir2 = p2 - p0; // 顶点到当前点向量差
var n1 = normalize(dir1); // 射线到顶点向量差归一化
var n2 = normalize(dir2); // 顶点到当前点向量差归一化
var t = dot(n1, n2); // 获取n1到n2余弦值
var d = dot(n1, dir2); // 获取n1到dir距离
var red = step(0.5, t); // 检查余弦是否大于0.5
red *= (1.0 - step(0.5, d)); // 检查距离是否大于0.5
return vec4f(red, 0.0, 0.0, 1.0);
}
结果
26. Clamp
clamp(uv.x, 0.25, 0.75);通过clamp限制像素闭区间,step(lineWidth * 0.5, dist)判断分量y和限制后分量x差值的绝对值是否在线宽范围内,有正负区间但是取得是绝对值所以与线宽对比时需要lineWidth * 0.5
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
uv.y = 1.0 - uv.y;
var lineWidth = 0.2;
var lineColor = vec3f(1.0, 0.3, 0.3);
var value = clamp(uv.x, 0.25, 0.75);
var dist = abs(uv.y - value);
var line = 1.0 - step(lineWidth * 0.5, dist);
return vec4f(lineColor * line, 1.0);
}
结果
27. Texture
@group(2) @binding(1) var texture: texture_2d<f32>;Bevy中纹理加载存放固定在@group(2) @binding(1)
@group(2) @binding(2) var texture_sampler: sampler;Bevy中采样器加载存放固定在@group(2) @binding(2)通过textureSample函数输入纹理和采样器,通过uv*宽高比的小数部分实现贴图的平铺
#import bevy_render::view::View
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(2) @binding(1) var texture: texture_2d<f32>;
@group(2) @binding(2) var texture_sampler: sampler;
@group(0) @binding(0) var<uniform> view: View;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let texture_uvs = in.uv;
var ratio = vec2f(view.viewport.z / view.viewport.w, 1.0);
let tex: vec4f = textureSample(texture, texture_sampler, fract(texture_uvs * ratio));
return tex;
}
结果
28. Texture Mirror
纹理镜像重复
#import bevy_render::view::View
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(2) @binding(1) var texture: texture_2d<f32>;
@group(2) @binding(2) var texture_sampler: sampler;
@group(0) @binding(0) var<uniform> view: View;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
uv *= vec2f(view.viewport.z / view.viewport.w, 1.0);
var tx = fract(uv.x);
var ty = fract(uv.y);
tx = mix(tx, 1.0 - tx, step(1.0, uv.x % 2.0));
ty = mix(ty, 1.0 - ty, step(1.0, uv.y % 2.0));
return textureSample(texture, texture_sampler, vec2(tx, ty));
}
结果
29. Time
Bevy globals中globals.time为启动至当前的时间,通过运算获得纹理动画
#import bevy_sprite::mesh2d_view_bindings::globals
#import bevy_render::view::View
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(2) @binding(1) var texture: texture_2d<f32>;
@group(2) @binding(2) var texture_sampler: sampler;
@group(0) @binding(0) var<uniform> view: View;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
uv.y = 1.0 - uv.y;
var t = sin(globals.time * 2.0) * 0.5 + 0.5;
var delta = 0.375 * t * step(0.3, uv.y);
uv.y = 1.0 - clamp(uv.y + delta, 0.0, 1.0);
return textureSample(texture, texture_sampler, uv);
}
结果
30. Sprite Animation
通过(uv + vec2(x, y)) * vec2(0.5, 0.25)显示每帧显示纹理不同部分区域实现sprite动画
#import bevy_sprite::mesh2d_view_bindings::globals
#import bevy_render::view::View
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(2) @binding(1) var texture: texture_2d<f32>;
@group(2) @binding(2) var texture_sampler: sampler;
@group(0) @binding(0) var<uniform> view: View;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
var frame = floor(globals.time / 0.1) % 8.0;
var x = frame % 2.0;
var y = floor(frame / 2.0) % 4.0;
return textureSample(texture, texture_sampler, (uv + vec2(x, y)) * vec2(0.5, 0.25));
}
结果
31. Radial Shutter Animation
结果
32. dFdx / dFdy
WGSL中与dFdx/dFdy等效的函数为dpdx/dpdy,通过函数对纹理描边
#import bevy_render::view::View
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(2) @binding(1) var texture: texture_2d<f32>;
@group(2) @binding(2) var texture_sampler: sampler;
@group(0) @binding(0) var<uniform> view: View;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
// 采样纹理
var texel = textureSample(texture, texture_sampler, uv);
// 计算亮度
var luminance = dot(texel.rgb, vec3f(0.2126, 0.7152, 0.0722));
// 计算亮度在 x 和 y 方向的偏导数
var dx = dpdx(luminance);
var dy = dpdy(luminance);
// 计算偏导数绝对值之和
var t = abs(dx) + abs(dy);
// 根据 t 的值混合颜色
var mixedColor = mix(texel, vec4f(1.0, 0.0, 0.0, 1.0), select(0.0, 1.0, t > 0.0));
return mixedColor;
}
结果
33. Cross
结果
34. Matrix
#import bevy_sprite::mesh2d_view_bindings::globals
#import bevy_render::view::View
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(0) @binding(0) var<uniform> view: View;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var uv = in.uv;
uv = uv * 2.0 - 1.0;
uv.x *= view.viewport.z / view.viewport.w;
var angle = globals.time * 2.0;
var X_x = cos(angle);
var X_y = sin(angle);
var X = vec2f(X_x, X_y);
var Y_x = -sin(angle);
var Y_y = cos(angle);
var Y = vec2f(Y_x, Y_y);
var M = mat2x2f(X, Y);
var P = vec2f(0.5);
P = P * M;
var t = 1.0 - smoothstep(0.24, 0.25, distance(P, uv));
return vec4(t, 0.0, 0.0, 1.0);
}
结果
35. Equirectangular texture
结果
36. Spherical coordinates
结果
37. Normal space
结果