"""Synthetic noisy DLSS-RR-style scene with a known noise-free answer, in the rr_ngx_probe fixture format. Left-handed view space, linear depth, render size == output size or output larger. Surfaces: a wall (stripes fixed in WORLD space), a disc (a sphere's normals) and a sky band. The colour is (diffuse + specular albedo) * smooth lighting times per-frame mean-preserving log-normal noise; the sky is noise-free. The camera may translate along world x (--camera-speed per frame): the guides then carry the exact motion vectors (previous minus current, render pixels) and the view matrix, and clean-.bin is the noise-free output-size image of that frame. clean.bin is frame 0's. --extents W1xH1,W2xH2,... makes the render size change per frame (cycling, each within --size): the guides are written at that frame's size and dynamic-extents.txt lists them (the probe's "dynamic-extents" mode). No game, no NVIDIA runtime. Output stays under ignored build/. """ import argparse import math import sys from pathlib import Path repo = Path(__file__).resolve().parents[2] sys.path.insert(0, str(repo / '.tools/python/vendor')) import numpy as np FOV_DEGREES = 60.0 def scene(width, height, cam_x=0.0, prev_cam_x=None): """Returns depth, normal, diffuse, specular, clean colour, sky mask and motion (render pixels, prev - current).""" if prev_cam_x is None: prev_cam_x = cam_x y, x = np.mgrid[0:height, 0:width].astype('f8') u, v = (x + 0.5) / width * 2 - 1, 1 - (y + 0.5) / height * 2 aspect = width / height tangent = math.tan(math.radians(FOV_DEGREES) / 2) # camera-space ray scale: x_view = u * aspect * tangent * z, y_view = v * tangent * z z = np.full((height, width), 10.0) normal = np.zeros((height, width, 3)); normal[..., 2] = -1.0 diffuse = np.empty((height, width, 3)); diffuse[:] = (0.60, 0.50, 0.40) # the disc: a world-fixed sphere of radius 0.9 centred at world (0.9, -0.5, 7.5) centre = np.array([0.9, -0.5, 7.5]); radius = 0.9 ray = np.stack([u * aspect * tangent, v * tangent, np.ones_like(u)], axis=-1) origin = np.array([cam_x, 0.0, 0.0]) oc = origin - centre b = np.einsum('ijk,k->ij', ray, oc); a = np.einsum('ijk,ijk->ij', ray, ray); c = float(oc @ oc - radius * radius) disc_term = b * b - a * c hit = disc_term > 0 t = np.where(hit, (-b - np.sqrt(np.maximum(disc_term, 0))) / a, 0) point = origin + ray * t[..., None] disc = hit & (t > 0) z[disc] = (t * ray[..., 2])[disc] # view z = t (the ray's z component is 1) normal[disc] = ((point - centre) / radius)[disc] world_x = cam_x + u * aspect * tangent * 10.0 stripe = (np.floor(world_x / 0.45).astype(int) % 2) == 0 diffuse[stripe & ~disc] = (0.35, 0.45, 0.55) diffuse[disc] = (0.80, 0.25, 0.20) sky = v > 0.7 z[sky] = 1.0e8; normal[sky] = 0.0; diffuse[sky] = 0.0; disc = disc & ~sky specular = np.full((height, width, 3), 0.04); specular[sky] = 0.0 # lighting is a function of the surface point (world x, screen-independent y), so it moves with the scene surf_x = np.where(disc, point[..., 0], world_x) light = (1.5 + 0.8 * np.sin(surf_x * 2.2) * np.cos(y * 360.0 / height / 41.0))[..., None] * np.array([1.0, 0.92, 0.8]) clean = (diffuse + specular) * light clean[sky] = (0.35, 0.55, 0.95) # motion: previous minus current screen position of the same surface point, in render pixels motion = np.zeros((height, width, 2)) dx = cam_x - prev_cam_x if dx != 0.0: depth = np.where(sky, 1.0, z) motion[..., 0] = dx / (depth * tangent * aspect) * (width / 2.0) motion[sky] = 0.0 return z, normal, diffuse, specular, clean, sky, motion def half_bytes(a): return np.ascontiguousarray(a.astype(' width or eh > height: parser.error('every extent must be at least 64 and within --size') rng = np.random.default_rng(args.seed) f = 1.0 / math.tan(math.radians(FOV_DEGREES) / 2) near, far = 0.1, 1000.0 lines = [f'size {width} {height} {out_width} {out_height}'] extent_lines = [] for i in range(args.frames): fw, fh = extents[i % len(extents)] extent_lines.append(f'{fw} {fh}') aspect = fw / fh proj = [f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, far / (far - near), 1, 0, 0, -near * far / (far - near), 0] cam = args.camera_speed * i z, normal, diffuse, specular, clean_render, sky, motion = scene(fw, fh, cam, cam - args.camera_speed if i else cam) if (out_width, out_height) == (fw, fh): clean_out, sky_out = clean_render, sky else: clean_out, sky_out = scene(out_width, out_height, cam)[4:6] noise = np.exp(args.noise * rng.standard_normal((fh, fw, 3)) - 0.5 * args.noise ** 2) noise[sky] = 1.0 color = np.concatenate([clean_render * noise, np.ones((fh, fw, 1))], axis=-1) rgba = lambda c3: np.concatenate([c3, np.zeros((fh, fw, 1))], axis=-1) (args.output / f'{i}-color.bin').write_bytes(half_bytes(color)) (args.output / f'{i}-motion.bin').write_bytes(half_bytes(motion)) (args.output / f'{i}-depth.bin').write_bytes(np.ascontiguousarray(z.astype(' view: translate by -camera lines.append('frame %d 0 0 1 1 1 %d 0 ' % (i, 1 if i == 0 else 0) + ' '.join(f'{v:.9g}' for v in view + proj)) (args.output / f'clean-{i}.bin').write_bytes(half_bytes(np.concatenate([clean_out, np.ones((out_height, out_width, 1))], axis=-1))) if (out_width, out_height) != (fw, fh): # the best a render-resolution result could be, before upscaling (args.output / f'clean-render-{i}.bin').write_bytes(half_bytes(np.concatenate([clean_render, np.ones((fh, fw, 1))], axis=-1))) if i == 0: (args.output / 'clean.bin').write_bytes((args.output / 'clean-0.bin').read_bytes()) np.save(args.output / 'sky.npy', sky_out) (args.output / 'frames.txt').write_text('\n'.join(lines) + '\n') if args.extents: (args.output / 'dynamic-extents.txt').write_text('\n'.join(extent_lines) + '\n') print('wrote', args.output) if __name__ == '__main__': main()