From 21e514c5607c81bcf699a0fa9ee8c9427f3852ab Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Mon, 2 Mar 2026 11:04:54 -0800 Subject: [PATCH] general sync up --- .gitignore | 4 + examples/capetown.py | 9 - examples/explore_zarr.py | 51 ++++- examples/generate_playground_gif.py | 331 ---------------------------- examples/guanajuato.py | 9 - examples/los_angeles.py | 10 - examples/playground.py | 1 + examples/rio.py | 9 - rtxpy/accessor.py | 42 +++- rtxpy/analysis/render.py | 25 ++- rtxpy/rtx.py | 9 +- rtxpy/viewer/render_settings.py | 3 + 12 files changed, 124 insertions(+), 379 deletions(-) delete mode 100644 examples/capetown.py delete mode 100644 examples/generate_playground_gif.py delete mode 100644 examples/guanajuato.py delete mode 100644 examples/los_angeles.py delete mode 100644 examples/rio.py diff --git a/.gitignore b/.gitignore index 93be806..a065fce 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,7 @@ examples/.ipynb_checkpoints/ *.png *.tif *.geojson +*.zarr/ +conda-output/ +examples/cache/ +*gtfs.json diff --git a/examples/capetown.py b/examples/capetown.py deleted file mode 100644 index 9d23fd8..0000000 --- a/examples/capetown.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Cape Town — GPU-accelerated terrain exploration.""" -from rtxpy import quickstart - -quickstart( - name='capetown', - bounds=(18.3, -34.2, 18.7, -33.8), - crs='EPSG:32734', - features=['buildings', 'roads', 'water', 'fire'], -) diff --git a/examples/explore_zarr.py b/examples/explore_zarr.py index 0b3d62d..8077681 100644 --- a/examples/explore_zarr.py +++ b/examples/explore_zarr.py @@ -239,13 +239,62 @@ def loader(cam_x, cam_y): loader = make_terrain_loader(args.zarr, args.size, args.subsample, args.lon, args.lat) - print(f"\nLaunching explore...\n") + # --- Hydro flow data (off by default, Shift+Y to toggle) --------------- + hydro = None + try: + from xrspatial import fill as _fill + from xrspatial import flow_direction as _flow_direction + from xrspatial import flow_accumulation as _flow_accumulation + from xrspatial import stream_order as _stream_order + from scipy.ndimage import uniform_filter as _uniform_filter + + print("Conditioning DEM for hydrological flow...") + _elev = cp.asnumpy(terrain.data).astype(np.float32) + _nodata = (_elev == 0.0) | np.isnan(_elev) + _elev[_nodata] = -100.0 + + _smoothed = _uniform_filter(_elev, size=15, mode='nearest') + _smoothed[_nodata] = -100.0 + + _sm = cp.asarray(_smoothed) + _filled = _fill(terrain.copy(data=_sm)) + _fd = _filled.data - _sm + _resolved = _filled.data + _fd * 0.01 + cp.random.seed(0) + _resolved += cp.random.uniform(0, 0.001, _resolved.shape, + dtype=cp.float32) + _resolved[cp.asarray(_nodata)] = -100.0 + + fd = _flow_direction(terrain.copy(data=_resolved)) + fa = _flow_accumulation(fd) + so = _stream_order(fd, fa, threshold=50) + + fd_out, fa_out, so_out = fd.data, fa.data, so.data + _nodata_gpu = cp.asarray(_nodata) + fd_out[_nodata_gpu] = cp.nan + fa_out[_nodata_gpu] = cp.nan + so_out[_nodata_gpu] = cp.nan + + hydro = { + 'flow_dir': fd_out, + 'flow_accum': fa_out, + 'stream_order': so_out, + 'accum_threshold': 50, + 'enabled': False, + } + print(f" Flow direction + accumulation computed on " + f"{terrain.shape[0]}x{terrain.shape[1]} grid") + except Exception as e: + print(f"Skipping hydro: {e}") + + print(f"\nLaunching explore (Shift+Y to toggle hydro)...\n") terrain.rtx.explore( width=2048, height=1600, render_scale=0.5, color_stretch='cbrt', terrain_loader=loader, + hydro_data=hydro, repl=True, ) diff --git a/examples/generate_playground_gif.py b/examples/generate_playground_gif.py deleted file mode 100644 index 051f497..0000000 --- a/examples/generate_playground_gif.py +++ /dev/null @@ -1,331 +0,0 @@ -"""Generate a GIF from the playground viewshed/hillshade animation. - -This script creates frames from the Crater Lake hiking animation and -combines them into a GIF suitable for the README. -""" - -import numpy as np -import cupy -import xarray as xr -from pathlib import Path -from PIL import Image -import io - -from rtxpy import RTX, viewshed, hillshade - - -def load_terrain(): - """Load Crater Lake terrain data.""" - import rioxarray as rxr - - dem_path = Path(__file__).parent / "crater_lake_national_park.tif" - - if not dem_path.exists(): - raise FileNotFoundError(f"DEM file not found at {dem_path}. Run playground.py first to download it.") - - print(f"Loading DEM: {dem_path}") - terrain = rxr.open_rasterio(str(dem_path), masked=True).squeeze() - - # Subsample aggressively for smaller GIF file size - terrain = terrain[::10, ::10] - - # Crop edges to remove invalid border values - crop = 20 - terrain = terrain[crop:-crop, crop:-crop] - - # Ensure contiguous array before GPU transfer - terrain.data = np.ascontiguousarray(terrain.data) - - # Convert to cupy for GPU processing - terrain.data = cupy.asarray(terrain.data) - - print(f"Terrain loaded: {terrain.shape}") - return terrain - - -def generate_hiking_path(x_coords, y_coords, num_points=360): - """Generate a hiking path around Crater Lake (roughly circular).""" - cx = (x_coords.min() + x_coords.max()) / 2 - cy = (y_coords.min() + y_coords.max()) / 2 - - rx = (x_coords.max() - x_coords.min()) * 0.25 - ry = (y_coords.max() - y_coords.min()) * 0.25 - - angles = np.linspace(0, 2 * np.pi, num_points) - wobble = np.sin(angles * 8) * 0.1 - - path_x = cx + (rx + rx * wobble) * np.cos(angles) - path_y = cy + (ry + ry * wobble) * np.sin(angles) - - return path_x, path_y - - -def coords_to_pixel(x, y, x_coords, y_coords): - """Convert data coordinates to pixel coordinates.""" - px = np.searchsorted(x_coords, x) - py = np.searchsorted(-y_coords, -y) - return int(np.clip(px, 0, len(x_coords) - 1)), int(np.clip(py, 0, len(y_coords) - 1)) - - -def draw_legend(colors, x=10, y=10): - """Draw a legend in the corner of the frame.""" - from PIL import Image as PILImage, ImageDraw, ImageFont - - H, W = colors.shape[:2] - - # Create a small PIL image for drawing text - legend_w, legend_h = 90, 52 - legend = PILImage.new('RGBA', (legend_w, legend_h), (0, 0, 0, 180)) - draw = ImageDraw.Draw(legend) - - # Use default font - try: - font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10) - except (OSError, IOError): - font = ImageFont.load_default() - - # Legend entries: color swatch + label - entries = [ - ((50, 220, 50), "Visible"), - ((80, 80, 85), "Not Visible"), - ((0, 255, 255), "Observer"), - ] - - for i, (color, label) in enumerate(entries): - row_y = 5 + i * 15 - # Draw color swatch - draw.rectangle([5, row_y, 15, row_y + 10], fill=color) - # Draw label - draw.text((20, row_y - 1), label, fill=(255, 255, 255), font=font) - - # Convert legend to numpy and overlay on frame - legend_arr = np.array(legend) - - # Blend legend onto frame - for ly in range(legend_h): - for lx in range(legend_w): - fy, fx = y + ly, x + lx - if 0 <= fy < H and 0 <= fx < W: - alpha = legend_arr[ly, lx, 3] / 255.0 - colors[fy, fx, :3] = ( - colors[fy, fx, :3] * (1 - alpha) + legend_arr[ly, lx, :3] * alpha - ).astype(np.uint8) - - -def draw_observer_marker(colors, px, py, radius=6, glow_radius=18): - """Draw a glowing teal marker with dark outline at the observer's position.""" - H, W = colors.shape[:2] - outline_width = 2 - - # Draw outer glow, dark outline, then bright center - for dy in range(-glow_radius, glow_radius + 1): - for dx in range(-glow_radius, glow_radius + 1): - dist_sq = dx*dx + dy*dy - if dist_sq <= glow_radius * glow_radius: - ny, nx = py + dy, px + dx - if 0 <= ny < H and 0 <= nx < W: - dist = np.sqrt(dist_sq) - if dist <= radius: - # Bright cyan/teal center - colors[ny, nx] = [0, 255, 255, 255] - elif dist <= radius + outline_width: - # Dark outline for contrast - colors[ny, nx] = [0, 40, 40, 255] - elif dist <= glow_radius: - # Glow falloff - blend cyan with existing color - t = (dist - radius - outline_width) / (glow_radius - radius - outline_width) - glow_strength = (1 - t) ** 1.5 # Slightly softer falloff - existing = colors[ny, nx, :3].astype(np.float32) - cyan = np.array([0, 255, 255], dtype=np.float32) - blended = existing + (cyan - existing) * glow_strength * 0.7 - colors[ny, nx, :3] = np.clip(blended, 0, 255).astype(np.uint8) - - -def generate_frames(terrain, num_frames=72): - """Generate animation frames. - - Parameters - ---------- - terrain : xarray.DataArray - The terrain data. - num_frames : int - Number of frames to generate. Both the hillshade and hiker will - complete exactly one full 360° loop in this many frames. - """ - H, W = terrain.data.shape - rtx = RTX() - - x_coords = terrain.indexes.get('x').values - y_coords = terrain.indexes.get('y').values - - path_x, path_y = generate_hiking_path(x_coords, y_coords, num_points=360) - - frames = [] - azimuth = 225 - - print(f"Generating {num_frames} frames...") - - # Calculate rotation per frame for full 360° loop - azimuth_step = 360 / num_frames - hiker_step = 360 / num_frames - - for frame_idx in range(num_frames): - path_idx = int((frame_idx * hiker_step) % 360) - - vsw = path_x[path_idx] - vsh = path_y[path_idx] - azimuth = (225 + frame_idx * azimuth_step) % 360 - - # Compute hillshade and viewshed - hs = hillshade(terrain, - shadows=True, - azimuth=azimuth, - angle_altitude=25, - rtx=rtx) - vs = viewshed(terrain, - x=vsw, - y=vsh, - observer_elev=100.0, - rtx=rtx) - - # Convert to numpy arrays - hs_data = hs.data.get() if hasattr(hs.data, 'get') else hs.data - vs_data = vs.data.get() if hasattr(vs.data, 'get') else vs.data - - # Track NaN and zero pixels before converting - these will be transparent - transparent_mask = np.isnan(hs_data) | np.isnan(vs_data) | (hs_data == 0) - - hs_data = np.nan_to_num(hs_data, nan=0.5) - gray = np.uint8(np.clip(hs_data * 200, 0, 255)) - - # Viewshed returns -1 for invisible, 0-180 for visible (angle) - visible_mask = vs_data >= 0 - not_visible_mask = (vs_data < 0) & ~transparent_mask - - # Compose the final image with alpha channel (RGBA) - colors = np.zeros((H, W, 4), dtype=np.uint8) - colors[:, :, 0] = gray - colors[:, :, 1] = gray - colors[:, :, 2] = gray - colors[:, :, 3] = 255 # Fully opaque by default - - # Tint visible areas bright lime green - make it really pop! - colors[visible_mask, 0] = 50 # Low red - colors[visible_mask, 1] = np.minimum(255, gray[visible_mask].astype(np.int16) + 120).astype(np.uint8) # Bright green - colors[visible_mask, 2] = 50 # Low blue - - # Tint non-visible areas darker gray - colors[not_visible_mask, 0] = (colors[not_visible_mask, 0] * 0.5).astype(np.uint8) - colors[not_visible_mask, 1] = (colors[not_visible_mask, 1] * 0.5).astype(np.uint8) - colors[not_visible_mask, 2] = (colors[not_visible_mask, 2] * 0.55).astype(np.uint8) - - # Make NaN and zero pixels transparent - colors[transparent_mask, 3] = 0 - - # Draw observer marker - px, py = coords_to_pixel(vsw, vsh, x_coords, y_coords) - draw_observer_marker(colors, px, py, radius=4) - - # Draw legend - draw_legend(colors, x=10, y=10) - - frames.append(Image.fromarray(colors, mode='RGBA')) - - if (frame_idx + 1) % 10 == 0: - print(f" Frame {frame_idx + 1}/{num_frames}") - - return frames - - -def create_gif(frames, output_path, fps=12, max_colors=64): - """Create a GIF from frames. - - Parameters - ---------- - frames : list of PIL.Image - The frames to combine. - output_path : Path - Output path for the GIF. - fps : int - Frames per second. - max_colors : int - Maximum colors in palette for smaller file size. - """ - duration = int(1000 / fps) # Duration in milliseconds - - # Use a magenta color as the transparency key (unlikely to appear in terrain) - transparent_color = (255, 0, 255) - - # Convert RGBA frames to RGB, replacing transparent pixels with the key color - print("Converting frames for GIF transparency...") - rgb_frames = [] - for frame in frames: - arr = np.array(frame) - rgb = arr[:, :, :3].copy() - alpha = arr[:, :, 3] - # Set transparent pixels to the key color - rgb[alpha == 0] = transparent_color - rgb_frames.append(Image.fromarray(rgb, mode='RGB')) - - # Create global palette from sampled frames to avoid flickering - print(f"Building global palette from {len(rgb_frames)} frames...") - sample_step = max(1, len(rgb_frames) // 10) - sampled = [np.array(rgb_frames[i]) for i in range(0, len(rgb_frames), sample_step)] - combined = np.concatenate([p.reshape(-1, 3) for p in sampled], axis=0) - - h, w = np.array(rgb_frames[0]).shape[:2] - sample_h = int(np.ceil(len(combined) / w)) - padded = np.zeros((sample_h * w, 3), dtype=np.uint8) - padded[:len(combined)] = combined - palette_img = Image.fromarray(padded.reshape(sample_h, w, 3), mode='RGB') - global_palette = palette_img.quantize(colors=max_colors, method=Image.Quantize.MEDIANCUT) - - # Modify palette to reserve index 0 for transparency - palette_data = list(global_palette.getpalette()) - palette_data[0:3] = transparent_color # Force index 0 to be transparent color - global_palette.putpalette(palette_data) - transparency_index = 0 - - # Quantize all frames using the global palette - print(f"Quantizing frames to {max_colors} colors...") - quantized_frames = [] - for frame in rgb_frames: - q_frame = frame.quantize(palette=global_palette, dither=Image.Dither.FLOYDSTEINBERG) - quantized_frames.append(q_frame) - - print(f"Creating GIF at {output_path}...") - save_kwargs = { - 'save_all': True, - 'append_images': quantized_frames[1:], - 'duration': duration, - 'loop': 0, # Loop forever - 'optimize': True - } - if transparency_index is not None: - save_kwargs['transparency'] = transparency_index - save_kwargs['disposal'] = 2 # Restore to background - print(f" Using transparency index: {transparency_index}") - - quantized_frames[0].save(output_path, **save_kwargs) - - file_size = output_path.stat().st_size / (1024 * 1024) - print(f"GIF created: {output_path} ({file_size:.1f} MB)") - - -def main(): - output_path = Path(__file__).parent / "images" / "playground_demo.gif" - output_path.parent.mkdir(exist_ok=True) - - terrain = load_terrain() - - # Generate 120 frames - both hillshade and hiker complete exactly one 360° loop - # At 15fps this gives an 8 second loop that repeats seamlessly - frames = generate_frames(terrain, num_frames=120) - - create_gif(frames, output_path, fps=6, max_colors=128) - - print(f"\nDone! GIF saved to: {output_path}") - - -if __name__ == "__main__": - main() diff --git a/examples/guanajuato.py b/examples/guanajuato.py deleted file mode 100644 index 561a82d..0000000 --- a/examples/guanajuato.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Guanajuato — GPU-accelerated terrain exploration.""" -from rtxpy import quickstart - -quickstart( - name='guanajuato', - bounds=(-101.50, 20.70, -100.50, 21.30), - crs='EPSG:32614', - features=['buildings', 'roads', 'water', 'fire'], -) diff --git a/examples/los_angeles.py b/examples/los_angeles.py deleted file mode 100644 index 4e5cf6a..0000000 --- a/examples/los_angeles.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Los Angeles — GPU-accelerated terrain exploration.""" -from rtxpy import quickstart - -quickstart( - name='los_angeles', - bounds=(-118.52, 33.85, -117.25, 34.23), - crs='EPSG:32611', - source='usgs_10m', - features=['buildings', 'roads', 'water', 'fire'], -) diff --git a/examples/playground.py b/examples/playground.py index 4beaa31..6e5f93c 100644 --- a/examples/playground.py +++ b/examples/playground.py @@ -327,6 +327,7 @@ def load_terrain(): render_scale=0.5, wind_data=wind, hydro_data=hydro, + fog_density=3.0, repl=True, ) diff --git a/examples/rio.py b/examples/rio.py deleted file mode 100644 index c901cda..0000000 --- a/examples/rio.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Rio de Janeiro — GPU-accelerated terrain exploration.""" -from rtxpy import quickstart - -quickstart( - name='rio', - bounds=(-43.42, -23.08, -43.10, -22.84), - crs='EPSG:32723', - features=['buildings', 'roads', 'water'], -) diff --git a/rtxpy/accessor.py b/rtxpy/accessor.py index 007ae2c..fdab96e 100644 --- a/rtxpy/accessor.py +++ b/rtxpy/accessor.py @@ -581,14 +581,16 @@ def trace(self, rays, hits, num_rays, primitive_ids=None, instance_ids=None): """ return self._rtx.trace(rays, hits, num_rays, primitive_ids, instance_ids) - def render(self, camera_position, look_at, fov=60.0, up=(0, 0, 1), + def render(self, camera_position=None, look_at=None, fov=60.0, up=(0, 0, 1), width=1920, height=1080, sun_azimuth=225, sun_altitude=45, shadows=True, ambient=0.15, fog_density=0.0, fog_color=(0.7, 0.8, 0.9), colormap='terrain', - color_range=None, output_path=None, alpha=False, + color_range=None, color_stretch='linear', output_path=None, + alpha=False, denoise=False, vertical_exaggeration=None, rtx=None, ao_samples=0, ao_radius=None, gi_bounces=1, sun_angle=0.0, - aperture=0.0, focal_distance=0.0): + aperture=0.0, focal_distance=0.0, + start_position=None): """Render terrain with a perspective camera for movie-quality visualization. Uses OptiX ray tracing to render terrain with realistic lighting, shadows, @@ -660,6 +662,14 @@ def render(self, camera_position, look_at, fov=60.0, up=(0, 0, 1), Distance to the focal plane. Objects at this distance are sharp. If 0, auto-computes from camera-to-lookat distance. Default is 0.0. + start_position : tuple of float, optional + Alias for ``camera_position`` (matches the explore() API). + If both are provided, ``camera_position`` takes precedence. + color_stretch : str, optional + Elevation color stretch: 'linear', 'sqrt', 'cbrt', or 'log'. + Default is 'linear'. + denoise : bool, optional + If True, apply OptiX AI Denoiser to the output. Default is False. Returns ------- @@ -676,6 +686,10 @@ def render(self, camera_position, look_at, fov=60.0, up=(0, 0, 1), ... output_path='terrain_render.png' ... ) """ + # start_position is an alias for camera_position (matches explore() API) + if camera_position is None: + camera_position = start_position + from .analysis import render as _render return _render( self._obj, @@ -693,8 +707,10 @@ def render(self, camera_position, look_at, fov=60.0, up=(0, 0, 1), fog_color=fog_color, colormap=colormap, color_range=color_range, + color_stretch=color_stretch, output_path=output_path, alpha=alpha, + denoise=denoise, vertical_exaggeration=vertical_exaggeration, rtx=rtx, # User can override, but default None creates fresh instance pixel_spacing_x=self._pixel_spacing_x, @@ -2550,6 +2566,9 @@ def explore(self, width=800, height=600, render_scale=0.5, subsample=1, wind_data=None, hydro_data=None, gtfs_data=None, terrain_loader=None, scene_zarr=None, ao_samples=0, gi_bounces=1, denoise=False, + fog_density=0.0, fog_color=(0.7, 0.8, 0.9), + colormap=None, sun_azimuth=None, sun_altitude=None, + shadows=None, ambient=None, repl=False, tour=None): """Launch an interactive terrain viewer with keyboard controls. @@ -2694,6 +2713,13 @@ def explore(self, width=800, height=600, render_scale=0.5, ao_samples=ao_samples, gi_bounces=gi_bounces, denoise=denoise, + fog_density=fog_density, + fog_color=fog_color, + colormap=colormap, + sun_azimuth=sun_azimuth, + sun_altitude=sun_altitude, + shadows=shadows, + ambient=ambient, repl=repl, tour=tour, ) @@ -3012,6 +3038,9 @@ def explore(self, z, width=800, height=600, render_scale=0.5, gtfs_data=None, scene_zarr=None, ao_samples=0, gi_bounces=1, denoise=False, + fog_density=0.0, fog_color=(0.7, 0.8, 0.9), + colormap=None, sun_azimuth=None, sun_altitude=None, + shadows=None, ambient=None, minimap_style=None, minimap_layer=None, minimap_colors=None, info_text=None, repl=False, tour=None): @@ -3122,6 +3151,13 @@ def explore(self, z, width=800, height=600, render_scale=0.5, ao_samples=ao_samples, gi_bounces=gi_bounces, denoise=denoise, + fog_density=fog_density, + fog_color=fog_color, + colormap=colormap, + sun_azimuth=sun_azimuth, + sun_altitude=sun_altitude, + shadows=shadows, + ambient=ambient, minimap_style=minimap_style, minimap_layer=minimap_layer, minimap_colors=minimap_colors, diff --git a/rtxpy/analysis/render.py b/rtxpy/analysis/render.py index c8b226a..c874a8a 100644 --- a/rtxpy/analysis/render.py +++ b/rtxpy/analysis/render.py @@ -1213,12 +1213,27 @@ def _shade_terrain_kernel( color_g = color_g * (1.0 - alpha) + 0.9 * alpha color_b = color_b * (1.0 - alpha) + 0.85 * alpha - # Fog + # Height-attenuated atmospheric fog + # Fog density decays exponentially with altitude: + # rho(z) = fog_density * exp(-b * (z - elev_min)) + # The optical depth along the ray is the analytic integral + # of rho over the camera-to-hit path, so valleys fill with + # haze while ridgelines stay crisp. if fog_density > 0: - fog_amount = 1.0 - math.exp(-fog_density * t) - color_r = color_r * (1 - fog_amount) + fog_color_r * fog_amount - color_g = color_g * (1 - fog_amount) + fog_color_g * fog_amount - color_b = color_b * (1 - fog_amount) + fog_color_b * fog_amount + b = 2.5 / (elev_range + 1e-6) + dz = hit_z - oz + if abs(dz) > 0.001: + exp_cam = math.exp(-b * (oz - elev_min)) + exp_hit = math.exp(-b * (hit_z - elev_min)) + optical_depth = fog_density * t * (exp_cam - exp_hit) / (b * dz) + else: + z_mid = (oz + hit_z) * 0.5 + optical_depth = fog_density * t * math.exp(-b * (z_mid - elev_min)) + if optical_depth > 0.0: + fog_amount = 1.0 - math.exp(-optical_depth) + color_r = color_r * (1 - fog_amount) + fog_color_r * fog_amount + color_g = color_g * (1 - fog_amount) + fog_color_g * fog_amount + color_b = color_b * (1 - fog_amount) + fog_color_b * fog_amount output[py, px, 0] = color_r output[py, px, 1] = color_g diff --git a/rtxpy/rtx.py b/rtxpy/rtx.py index 10bc5de..d5d7356 100644 --- a/rtxpy/rtx.py +++ b/rtxpy/rtx.py @@ -1069,6 +1069,11 @@ def _build_gas_clustered(vertices, indices, grid_H, grid_W): # Upload args to GPU d_args = cupy.asarray(np.frombuffer(args_host, dtype=np.uint8)) + # Compute actual totals across all clusters (boundary vertices are + # duplicated in per-cluster sub-buffers, so the sum exceeds num_vertices). + total_cluster_verts = sum(len(v) for v in verts_per_cluster) + total_cluster_tris = sum(len(a) // 3 for a in cluster_index_arrays) + # -- Phase 1: Build clusters from triangles ---------------------------- cluster_build_input = { 'type': int(optix.CLUSTER_ACCEL_BUILD_TYPE_CLUSTERS_FROM_TRIANGLES), @@ -1080,8 +1085,8 @@ def _build_gas_clustered(vertices, indices, grid_H, grid_W): 'maxUniqueSbtIndexCountPerArg': 1, 'maxTriangleCountPerArg': max_tri_per_cluster, 'maxVertexCountPerArg': max_vert_per_cluster, - 'maxTotalTriangleCount': num_triangles, - 'maxTotalVertexCount': num_vertices, + 'maxTotalTriangleCount': total_cluster_tris, + 'maxTotalVertexCount': total_cluster_verts, 'minPositionTruncateBitCount': 0, }, } diff --git a/rtxpy/viewer/render_settings.py b/rtxpy/viewer/render_settings.py index 618322e..b7002ea 100644 --- a/rtxpy/viewer/render_settings.py +++ b/rtxpy/viewer/render_settings.py @@ -11,6 +11,7 @@ class RenderSettings: __slots__ = ( 'shadows', 'ambient', 'sun_azimuth', 'sun_altitude', + 'fog_density', 'fog_color', 'colormap', 'colormaps', 'colormap_idx', 'color_stretch', '_color_stretches', '_color_stretch_idx', 'ao_enabled', 'ao_radius', 'gi_intensity', 'gi_bounces', @@ -26,6 +27,8 @@ def __init__(self): self.ambient = 0.2 self.sun_azimuth = 225.0 self.sun_altitude = 35.0 + self.fog_density = 0.0 + self.fog_color = (0.7, 0.8, 0.9) self.colormap = 'gray' self.colormaps = ['gray', 'terrain', 'viridis', 'plasma', 'cividis'] self.colormap_idx = 0