Shadertoy学习 vol. 5.5 Ray Marching 番外篇

result

不好意思,最近申请太忙了,没什么时间更新,只好拿一个我的课堂小作业来水。基本内容还是跟上次的Ray Marching一样,不过这次拿python来写一下,把图像拿ASCII字符渲染打到控制台去。

代码在此

这个作业不让import外部包,所以有一些地方得自己实现基础功能了。

线性代数计算

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
class Vector():
def __init__(self, array):
self.array = array
self.row = len(array)
self.column = -1

def __str__(self):
c = "["
for i in range(self.row):
if i == self.row - 1:
c += " " + str(self.array[i])
elif i == 0:
c += str(self.array[i])
else:
c += " " + str(self.array[i])
c += "]"
return c

def __add__(self, other):
c = Vector([0 for i in range(self.row)])
for i in range(0, self.row):
c.array[i] = self.array[i] + other.array[i]
return c

def __sub__(self, other):
c = Vector([0 for i in range(self.row)])
for i in range(0, self.row):
c.array[i] = self.array[i] - other.array[i]
return c

def __mul__(self, other):
c = Vector([0 for i in range(self.row)])
for i in range(0, self.row):
c.array[i] = self.array[i] * other
return c

def __truediv__(self, other):
c = Vector([0 for i in range(self.row)])
for i in range(0, self.row):
c.array[i] = self.array[i] / other
return c

def divVec(self, other):
c = Vector([0 for i in range(self.row)])
for i in range(0, self.row):
c.array[i] = self.array[i] / other.array[i]
return c

def innerProd(self, other):
c = 0
for i in range(0, self.row):
c += self.array[i] * other.array[i]
return c

def outerProd(self, other):
c = Matrix([[0 for j in range(self.row)] for i in range(self.row)])
for i in range(0, self.row):
for j in range(0, self.row):
c.array[i][j] = self.array[i] * other.array[j]
return c

def length(self):
if (len(self.array) == 3):
return (self.array[0]**2 + self.array[1]**2 +
self.array[2]**2)**0.5

def normalize(self):
c = Vector([0 for i in range(self.row)])
norm = self.length()
for i in range(0, self.row):
c.array[i] = self.array[i] / norm
return c


class Matrix():
def __init__(self, array):
self.array = array
self.row = len(array)
self.column = len(array[0])

def __str__(self):

maxlen = [0 for j in range(self.column)]
for i in range(0, self.row):
for j in range(0, self.column):
if len(str(self.array[i][j])) > maxlen[j]:
maxlen[j] = len(str(self.array[i][j]))

a = "["
for i in range(0, self.row):
if i == 0:
a += "["
else:
a += " ["
for j in range(0, self.column):
extraspace = maxlen[j] - len(str(self.array[i][j]))
if j == self.column - 1:
a += " " * extraspace + str(self.array[i][j])
elif j == 0:
a += " " + " " * extraspace + str(self.array[i][j]) + " "
else:
a += " " * extraspace + str(self.array[i][j]) + " "
if i == self.row - 1:
a += ']'
else:
a += ']' + "\n"
a += ']'
return a

def __add__(self, other):
c = Matrix([[0 for j in range(self.column)] for i in range(self.row)])
for i in range(0, self.row):
for j in range(0, self.column):
c.array[i][j] = self.array[i][j] + other.array[i][j]
return c

def __sub__(self, other):
c = Matrix([[0 for j in range(self.column)] for i in range(self.row)])
for i in range(0, self.row):
for j in range(0, self.column):
c.array[i][j] = self.array[i][j] - other.array[i][j]
return c

def __mul__(self, other):
if other.column == -1:
c = Vector([1 for i in range(self.row)])
for i in range(0, self.row):
c.array[i] = self.__mulVec(other, i)
return c
else:
c = Matrix([[0 for j in range(other.column)]
for i in range(self.row)])
for i in range(0, c.row):
for j in range(0, c.column):
c.array[i][j] = self.__mulEle(other, i, j)
return c

def __mulEle(self, other, i, j):
ele = 0
for k in range(0, self.column):
ele += self.array[i][k] * other.array[k][j]
return ele

def __mulVec(self, other, i):
ele = 0
for k in range(0, self.column):
ele += self.array[i][k] * other.array[k]
return ele

def eleWise(self, other):
c = Matrix([[0 for j in range(self.column)] for i in range(self.row)])
for i in range(0, self.row):
for j in range(0, self.column):
c.array[i][j] = self.array[i][j] * other.array[i][j]
return c


''' Examples
a = Matrix([[1, 2, 5], [5, 0, 2], [1, 3, 4]])
b = Matrix([[3, 4, 1], [0, 2, 1], [2, 3, 7]])
c = Vector([2, 3, 1])
d = Vector([2, 4, 2])

print("-----" * 20)
print(a + b, "\n")
print(a - b, "\n")
print(a * b, "\n")
print(a * c, "\n")
print(Matrix.eleWise(a, b), "\n")
print(Vector.innerProd(c, d), "\n")
print(Vector.outerProd(c, d), "\n")
'''

写了两个类,一个Vector, 一个Matrix。这部分没什么值得提的,有需要可以换成numpy来算。不过有很多地方就得改。

屏幕

这个程序还是由CPU计算的,一方面是因为我没有什么GPU方面的编程经验,另一方面则是这个程序还是不让引用外部包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Screen():
def __init__(self, width, height):
self.width = width
self.height = height
self.fragCoord = Matrix(
[[Vector([j + 0.5, i + 0.5]) for j in range(width)]
for i in range(height - 1, -1, -1)])
self.resolution = Vector([self.width, self.height])
self.fragColor = Matrix([[Vector([0]) for j in range(width)]
for i in range(height)])

self.uv = self.fragCoord
for i in range(height):
for j in range(width):
self.uv.array[i][j] = Vector.divVec(
self.fragCoord.array[i][j] - self.resolution * 0.5,
self.resolution)

这里我设置了两个二维数组,fragcoord里面存放的是归一化了的像素坐标,fragcoolor则存放之后计算好的像素值

RayMarch

这部分没有任何变化,只是把之前的GLSL用python重写了一遍。

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
SURFACE_DIST = 0.01
MAX_DIST = 150.0
MAX_STEP = 100


def clamp(x, low, high):
return min(max(x, low), high)


def getDist(p):
s = Vector([0, 1, 6])
sRadius = 1.0
sphereDist = Vector.length(p - s) - sRadius

planeDist = p.array[1]

dist = min(sphereDist, planeDist)
return dist


def rayMarch(ro, rd):
depth = 0
for _ in range(MAX_STEP):
pos = ro + rd * depth
dist = getDist(pos)
depth += dist
if dist < SURFACE_DIST or depth > MAX_DIST:
break
return depth


def getNormal(p):
dx = Vector([0.01, 0.0, 0.0])
dy = Vector([0.0, 0.01, 0.0])
dz = Vector([0.0, 0.0, 0.01])
dist = getDist(p)
n = Vector([
dist - getDist(p - dx), dist - getDist(p - dy), dist - getDist(p - dz)
])
return (n.normalize())


def getLight(p):
lightPos = Vector([3, 5, -1])
li = Vector.normalize(lightPos - p)
n = getNormal(p)

dif = clamp(Vector.innerProd(n, li), 0, 1)

if p.array[1] < 0.1:
dif *= float(int(p.array[0] + 100) % 2
^ int(p.array[2] + 100) % 2) * 0.9

d = rayMarch(p + n * (SURFACE_DIST * 2), li)
if d < Vector.length(lightPos - p):
dif *= 0.1
return dif

字符转化

因为要打到控制台去,并没有GUI,所以我们会根据灰度值来对应字符。我直接去网上找了个别人列好的ASCII字符表,然后只要按顺序把fragcoolor里的浮点数转换成字符就好了。

1
2
3
4
5
6
7
8
class Screen():
def turn2Chara(self, number):
charas = list('''@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxr\
jft/\\|()1{}[]?-_+~<>i!lI;:,"^`'. ''')
charas_len = len(charas)

chara = charas[int(number * charas_len)]
return chara

当然这里存在一个问题,我们直接把线性色彩给搞成灰度值了。照理来说我们会先把线性色彩给映射到SRGB空间,然后再合并通道的,直接换成灰色的会让一些微小的颜色差异消失,尤其当我们只有这几个ASCII字符时。不过这就是个简单的小作业,所以我懒得整了。有兴趣的可以参考 Shadertoy学习 vol3. 抗锯齿,SRGB,镜面反射 里的SRGB那一节。

并行计算

虽然这个作业不让引用外部库,但我还是毅然决然地加上了并行计算的部分。我TM直接import multiprocessing

一方面是只用CPU太慢了,另一方面是我想顺便试试python中的并行计算。

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
def calcPart(self, start, end):
result = Matrix([[Vector([0]) for j in range(self.width)]
for i in range(self.height)])
for i in range(start, end):
for j in range(self.width):
uv = self.uv.array[i][j]
color = Vector([0, 0, 0])
ro = Vector([0, 1, 0])
rd = Vector([uv.array[0], uv.array[1], 1]).normalize()

d = rayMarch(ro, rd)

p = ro + rd * d
dif = getLight(p)

color = Vector([dif])
result.array[i][j] = color
return result

def calcMul(self, processorNum):
from multiprocessing import Pool
start, interval = 0, int(self.height / processorNum)
pool = Pool(processes=processorNum)
processes = []

for _ in range(processorNum):
processes.append(
pool.apply_async(self.calcPart, (start, start + interval)))
start += interval
pool.close()
pool.join()

for process in processes:
self.fragColor += process.get()

这里为了避免同时对变量进行修改造成问题,我就简单粗暴地生成了多个实例,分别算完后再把结果组合在一起。

地板

封面的地板是方砖形的,之前看iq大神的样例时就想给我的也加上,这次没看他的思路,自己想了想,用异或运算写的。大概就是x轴坐标和z轴坐标的整数部分奇偶不同时给颜色值乘上个系数

如果有不用if来判断是否在平面上的方法,请务必在评论区告诉我!

1
2
if p.array[1] < 0.1:
dif *= float(int(p.array[0] + 100) % 2 ^ int(p.array[2] + 100) % 2) * 0.9

结语

申请季实在是有点忙,下期一定不水,敬请期待!

Comments