Skip to content

Commit fd74204

Browse files
committed
added example files
1 parent 67ccfaa commit fd74204

5 files changed

Lines changed: 746 additions & 0 deletions

File tree

examples/cuda_utils.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import numba as nb
2+
import numpy as np
3+
4+
def calc_dims(shape):
5+
threadsperblock = (32,32)
6+
blockspergrid = (
7+
(shape[0] + (threadsperblock[0] - 1)) // threadsperblock[0],
8+
(shape[1] + (threadsperblock[1] - 1)) // threadsperblock[1]
9+
)
10+
return blockspergrid, threadsperblock
11+
12+
@nb.cuda.jit(device=True)
13+
def add(a, b):
14+
return float3(a[0]+b[0], a[1]+b[1], a[2]+b[2])
15+
16+
@nb.cuda.jit(device=True)
17+
def diff(a, b):
18+
return float3(a[0]-b[0], a[1]-b[1], a[2]-b[2])
19+
20+
@nb.cuda.jit(device=True)
21+
def mul(a, b):
22+
return float3(a[0]*b, a[1]*b, a[2]*b)
23+
24+
@nb.cuda.jit(device=True)
25+
def multColor(a, b):
26+
return float3(a[0]*b[0], a[1]*b[1], a[2]*b[2])
27+
28+
@nb.cuda.jit(device=True)
29+
def dot(a, b):
30+
return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
31+
32+
@nb.cuda.jit(device=True)
33+
def mix(a, b, k):
34+
return add(mul(a, k), mul(b, 1-k))
35+
36+
@nb.cuda.jit(device=True)
37+
def make_float3(a, offset):
38+
return float3(a[offset], a[offset+1], a[offset+2])
39+
40+
@nb.cuda.jit(device=True)
41+
def invert(a):
42+
return float3(-a[0], -a[1], -a[2])
43+
44+
@nb.cuda.jit(device=True)
45+
def float3(a, b, c):
46+
return (np.float32(a), np.float32(b), np.float32(c))

examples/hillshade.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import numpy as np
2+
import numba as nb
3+
4+
import cupy
5+
import xarray as xr
6+
from rtxpy import RTX
7+
8+
from typing import Optional
9+
10+
from scipy.spatial.transform import Rotation as R
11+
12+
from raytrace.cuda_utils import *
13+
from raytrace import mesh_utils
14+
15+
@nb.cuda.jit
16+
def _generatePrimaryRays(data, x_coords, y_coords, H, W):
17+
"""
18+
A GPU kernel that given a set of x and y discrete coordinates on a raster terrain
19+
generates in @data a list of parallel rays that represent camera rays generated from an ortographic camera
20+
that is looking straight down at the surface from an origin height 10000
21+
"""
22+
i, j = nb.cuda.grid(2)
23+
if i>=0 and i < H and j>=0 and j < W:
24+
#data[i,j,0] = j + 1e-6 # x_coords[j] + 1e-6
25+
#data[i,j,1] = i + 1e-6 # y_coords[i] + 1e-6
26+
27+
if (j == W-1):
28+
data[i,j,0] = j - 1e-3
29+
else:
30+
data[i,j,0] = j + 1e-3
31+
32+
if (i == H-1):
33+
data[i,j,1] = i - 1e-3
34+
else:
35+
data[i,j,1] = i + 1e-3
36+
37+
data[i,j,2] = 10000 # Location of the camera (height)
38+
data[i,j,3] = 1e-3
39+
data[i,j,4] = 0
40+
data[i,j,5] = 0
41+
data[i,j,6] = -1
42+
data[i,j,7] = np.inf
43+
44+
45+
def generatePrimaryRays(rays, x_coords, y_coords, H, W):
46+
griddim, blockdim = calc_dims((H, W))
47+
_generatePrimaryRays[griddim, blockdim](rays, x_coords, y_coords, H, W)
48+
return 0
49+
50+
51+
@nb.cuda.jit
52+
def _generateShadowRays(rays, hits, normals, H, W, sunDir):
53+
"""
54+
A GPU kernel that given a set rays and their respective intersection points,
55+
generates in rays (overwriting the original content) a new set of rays (shadow rays)
56+
That have their origins at the point of intersection of their parent ray and direction - the direction towards the sun
57+
The normals vectors at the point of intersection of the original rays are cached in @normals
58+
Thus we can later use them to do lambertian shading, after the shadow rays have been traced
59+
"""
60+
i, j = nb.cuda.grid(2)
61+
if i>=0 and i < H and j>=0 and j < W:
62+
dist = hits[i,j,0]
63+
norm = make_float3(hits[i,j], 1)
64+
if (norm[2] < 0):
65+
norm = invert(norm)
66+
ray = rays[i,j]
67+
rayOrigin = make_float3(ray, 0)
68+
rayDir = make_float3(ray, 4)
69+
p = add(rayOrigin, mul(rayDir,dist))
70+
71+
newOrigin = add(p, mul(norm, 1e-3))
72+
ray[0] = newOrigin[0]
73+
ray[1] = newOrigin[1]
74+
ray[2] = newOrigin[2]
75+
ray[3] = 1e-3
76+
ray[4] = sunDir[0]
77+
ray[5] = sunDir[1]
78+
ray[6] = sunDir[2]
79+
ray[7] = np.inf if dist > 0 else 0
80+
81+
normals[i,j,0] = norm[0]
82+
normals[i,j,1] = norm[1]
83+
normals[i,j,2] = norm[2]
84+
85+
def generateShadowRays(rays, hits, normals, H, W, sunDir):
86+
griddim, blockdim = calc_dims((H, W))
87+
_generateShadowRays[griddim, blockdim](rays, hits, normals, H, W, sunDir)
88+
return 0
89+
90+
91+
@nb.cuda.jit
92+
def _shadeLambert(hits, normals, output, H, W, sunDir, castShadows):
93+
"""
94+
This kernel does a simple Lambertian shading
95+
The hits array contains the results of tracing the shadow rays through the scene.
96+
If the value in hits[x,y,0] is > 0, then a valid intersection occurred and that means that the point
97+
at location x,y is in shadow.
98+
The normals array stores the normal at the intersecion point of each camera ray
99+
We then use the information for light visibility and normal to apply Lambert's cosine law
100+
The final result is stored in output which is an RGB array
101+
"""
102+
i, j = nb.cuda.grid(2)
103+
if i>=0 and i < H and j>=0 and j < W:
104+
# Normal at the intersection of camera ray (i,j) with the scene
105+
norm = make_float3(normals[i,j], 0)
106+
107+
# Below is same as existing algorithm without shadows and is OK with shadows.
108+
# Could be improved with a bit of antialiasing at edges of shadow???
109+
110+
light_dir = make_float3(sunDir, 0) # Might have to make it zero if back cull.
111+
cos_theta = dot(light_dir, norm) # light_dir and norm are already normalised.
112+
113+
temp = (cos_theta + 1) / 2
114+
115+
if castShadows and hits[i, j, 0] >= 0:
116+
temp = temp / 2
117+
118+
if temp > 1:
119+
temp = 1
120+
elif temp < 0:
121+
temp = 0
122+
123+
output[i, j] = temp
124+
125+
126+
127+
def shadeLambert(hits, normals, output, H, W, sunDir, castShadows):
128+
griddim, blockdim = calc_dims((H, W))
129+
_shadeLambert[griddim, blockdim](hits, normals, output, H, W, sunDir, castShadows)
130+
return 0
131+
132+
133+
def getSunDir(angle_altitude, azimuth):
134+
"""
135+
Calculate the vector towards the sun based on sun altitude angle and azimuth
136+
"""
137+
north = (0,1,0)
138+
rx = R.from_euler('x', angle_altitude, degrees=True)
139+
rz = R.from_euler('z', azimuth+180, degrees=True)
140+
sunDir = rx.apply(north)
141+
sunDir = rz.apply(sunDir)
142+
return sunDir
143+
144+
145+
def hillshade_rt(raster: xr.DataArray,
146+
optix: RTX,
147+
shadows: bool = False,
148+
azimuth: int = 225,
149+
angle_altitude: int = 25,
150+
name: Optional[str] = 'hillshade') -> xr.DataArray:
151+
152+
H,W = raster.shape
153+
sunDir = cupy.array(getSunDir(angle_altitude, azimuth))
154+
155+
#output = np.zeros((H,W,3), np.float32)
156+
157+
# Device buffers
158+
d_rays = cupy.empty((H,W,8), np.float32)
159+
d_hits = cupy.empty((H,W,4), np.float32)
160+
d_aux = cupy.empty((H,W,3), np.float32)
161+
d_output = cupy.empty((H,W), np.float32)
162+
163+
y_coords = cupy.array(raster.indexes.get('y').values)
164+
x_coords = cupy.array(raster.indexes.get('x').values)
165+
166+
generatePrimaryRays(d_rays, x_coords, y_coords, H, W)
167+
device = cupy.cuda.Device(0)
168+
device.synchronize()
169+
res = optix.trace(d_rays, d_hits, W*H)
170+
171+
generateShadowRays(d_rays, d_hits, d_aux, H, W, sunDir)
172+
if shadows:
173+
device.synchronize()
174+
res = optix.trace(d_rays, d_hits, W*H)
175+
176+
shadeLambert(d_hits, d_aux, d_output, H, W, sunDir, shadows)
177+
178+
if isinstance(raster.data, np.ndarray):
179+
output = cupy.asnumpy(d_output[:, :])
180+
nanValue = np.nan
181+
else:
182+
output = d_output[:, :]
183+
nanValue = cupy.nan
184+
185+
output[0, :] = nanValue
186+
output[-1, :] = nanValue
187+
output[:, 0] = nanValue
188+
output[:, -1] = nanValue
189+
190+
hill = xr.DataArray(output,
191+
name=name,
192+
coords=raster.coords,
193+
dims=raster.dims,
194+
attrs=raster.attrs)
195+
return hill
196+
197+
def hillshade_gpu(raster: xr.DataArray,
198+
shadows: bool = False,
199+
azimuth: int = 225,
200+
angle_altitude: int = 25,
201+
name: Optional[str] = 'hillshade') -> xr.DataArray:
202+
# Move the terrain to GPU for testing the GPU path
203+
if not isinstance(raster.data, cupy.ndarray):
204+
print("WARNING: raster.data is not a cupy array. Additional overhead will be incurred")
205+
H,W = raster.shape
206+
optix = RTX()
207+
208+
datahash = np.uint64(hash(str(raster.data.get())))
209+
optixhash = np.uint64(optix.getHash())
210+
if (optixhash != datahash):
211+
numTris = (H - 1) * (W - 1) * 2
212+
verts = cupy.empty(H * W * 3, np.float32)
213+
triangles = cupy.empty(numTris * 3, np.int32)
214+
215+
# Generate a mesh from the terrain (buffers are on the GPU, so generation happens also on GPU)
216+
res = mesh_utils.triangulateTerrain(verts, triangles, raster)
217+
if res:
218+
raise ValueError("Failed to generate mesh from terrain. Error code:{}".format(res))
219+
res = optix.build(datahash, verts, triangles)
220+
if res:
221+
raise ValueError("OptiX failed to build GAS with error code:{}".format(res))
222+
#Clear some GPU memory that we no longer need
223+
verts = None
224+
triangles = None
225+
cupy.get_default_memory_pool().free_all_blocks()
226+
227+
hill = hillshade_rt(raster, optix, azimuth=azimuth, angle_altitude=angle_altitude, shadows=shadows, name=name)
228+
return hill

0 commit comments

Comments
 (0)