Repository navigation
Expand file tree
/
Copy pathvulkan_ocean_minimal.cpp
More file actions
291 lines (262 loc) · 14.3 KB
/
Copy pathvulkan_ocean_minimal.cpp
File metadata and controls
291 lines (262 loc) · 14.3 KB
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
// Vulkan deferred — minimal ocean.
//
// The "fancy water" (3-cascade Phillips/FFT-displaced surface with foam and
// transmission) with NO hero object: just an env HDR, a sand floor, and an
// Ocean. Where examples/vulkan/vulkan_ocean.cpp wraps the same surface in a
// boat, lighthouse, archipelago, LIDAR and radar, this shows that the water
// itself is fully standalone — the whole scene is a handful of lines built on
// the first-party threepp::Ocean type (objects/Ocean.hpp).
//
// The adaptive vertex warp follows the orbit target (the point you're looking
// at), demonstrating that the density focus is just a world coordinate — it was
// never tied to the boat; here it's the camera.
//
// Controls: drag to orbit, scroll to zoom.
// Up/Down — wind speed ±1 m/s (live; the sea morphs into the new state)
// Left/Right — wind direction ∓15°
// Headless capture: vulkan_ocean_minimal --shot <name.png> [--frames N] [--pt]
// (plus --cam x,y,z / --look x,y,z to reframe with no rebuild, and
// --wind <m/s> to capture a specific sea state).
// Validation hooks:
// --pond — 16 m garden-pond preset: shallow sandy bottom + rocks,
// calm wind. Exercises the scale-aware Ocean defaults and
// the shallow-water (visible-bottom) transmission path.
// --wind2 <m/s> — setWind() to this halfway through a --shot run; exercises
// the live-wind respectrum path headlessly.
// --probes — 3×3 emissive spheres pinned to sampleHeight() around the
// orbit target: the CPU/GPU wave-field parity check (spheres
// must sit ON the surface), and a lazy-height-readback smoke
// test in one flag.
// --bench <N> — headless per-pass GPU timing: collect N frames after a
// 240-frame warm-up and print median/mean for each timestamp
// bracket (ocean FFT/displace/foam/BLAS, TLAS refit, gbuffer,
// shade, denoise, TAA) plus the unbracketed residual. Run with
// THREEPP_VULKAN_SUPPRESS_PRESENT=1 so the wall columns mean
// something.
#include "threepp/loaders/RGBELoader.hpp"
#include "threepp/materials/MeshStandardMaterial.hpp"
#include "threepp/objects/Ocean.hpp"
#include "threepp/renderers/VulkanRenderer.hpp"
#include "threepp/threepp.hpp"
#include "capture_util.hpp"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <string>
using namespace threepp;
int main(int argc, char** argv) {
// ── Headless capture (dev iteration): N warm-up frames then one PNG ──────
std::string shotPath;
int shotFrames = 240, shotFrame = 0;
float windOverride = -1.f;// <0 = keep the Ocean default
float wind2 = -1.f; // ≥0 = setWind() to this halfway through a --shot run (live-wind test)
bool probes = false; // sampleHeight parity probes (debug)
bool pond = false; // 16 m pond preset: shallow bright bottom, calm wind
int benchFrames = 0; // >0 = --bench: collect N per-pass GPU timing samples, print, exit
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--shot") == 0 && i + 1 < argc) shotPath = argv[++i];
else if (std::strcmp(argv[i], "--frames") == 0 && i + 1 < argc) shotFrames = std::atoi(argv[++i]);
else if (std::strcmp(argv[i], "--wind") == 0 && i + 1 < argc) windOverride = float(std::atof(argv[++i]));
else if (std::strcmp(argv[i], "--wind2") == 0 && i + 1 < argc) wind2 = float(std::atof(argv[++i]));
else if (std::strcmp(argv[i], "--probes") == 0) probes = true;
else if (std::strcmp(argv[i], "--pond") == 0) pond = true;
else if (std::strcmp(argv[i], "--bench") == 0 && i + 1 < argc) benchFrames = std::atoi(argv[++i]);
}
const capture::Args capArgs = capture::parseArgs(argc, argv);
if (capArgs.frames) shotFrames = *capArgs.frames;
// --bench runs headless so THREEPP_VULKAN_SUPPRESS_PRESENT=1 can engage
// (present to a hidden window host-blocks ~a GPU frame and poisons the
// wall-clock columns; the per-pass GPU brackets don't care, but cpuFrame /
// gpuTotal do).
Canvas canvas("Vulkan Deferred - Ocean (minimal)",
{{"vsync", false}, {"size", WindowSize{1600, 900}}, {"headless", benchFrames > 0}});
auto renderer = VulkanRenderer(canvas);
renderer.setDenoise(true);
renderer.setRestirDIEnabled(true);
renderer.setFireflyClamp(6.0f);
// Trace at slightly reduced resolution; TAA upsamples by accumulating
// jittered low-res samples into the full-res history.
renderer.setRenderScale(0.9f);
renderer.toneMapping = ToneMapping::ACESFilmic;
renderer.toneMappingExposure = 0.7f;
RGBELoader rgbe;
auto env = rgbe.load(std::string(DATA_FOLDER) +
"/textures/env/autumn_field_puresky_2k.hdr");
Scene scene;
if (env) {
scene.background = env; // sky shows wherever rays miss the water
scene.environment = env; // image-based lighting for the surface
}
// Gentle sun — the HDR already carries one (env CDF + MIS importance-sample
// it); this mostly drives the photon-mapped caustics on the sand floor.
auto sun = DirectionalLight::create(Color(1.0f, 0.95f, 0.85f), 2.0f);
sun->position.set(2.f, 1.f, 2.f);
Object3D sunTarget;
sunTarget.position.set(0.f, 0.f, 0.f);
sun->setTarget(sunTarget);
scene.add(sun);
// --pond swaps the 1 km deep ocean for a 16 m garden pond: shallow bright
// sandy bottom with a few rocks, calm wind. Exercises the scale-aware
// Ocean defaults (auto tiles → dm-scale ripples) and the shallow-water
// transmission (the bottom is genuinely SEEN through the surface via the
// refracted-ray path in deferred_shade — not a repainted water body).
const float kSize = pond ? 16.0f : 1000.0f;
// Floor under the water. Ocean: near-black 5 m down — reads as deep water
// (the analytic body dominates). Pond: bright sand 1.2 m down so the
// shallow transmission has something worth refracting to.
auto floorMat = MeshStandardMaterial::create(
pond ? MeshStandardMaterial::Params{}.color(Color(0.55f, 0.42f, 0.24f)).roughness(1.0f)
: MeshStandardMaterial::Params{}.color(Color(0.02f, 0.02f, 0.02f)).roughness(1.0f));
auto floor = Mesh::create(PlaneGeometry::create(pond ? kSize * 3.f : kSize,
pond ? kSize * 3.f : kSize),
floorMat);
floor->rotation.x = -math::PI / 2.f;
floor->position.y = pond ? -1.2f : -5.f;
scene.add(floor);
if (pond) {
// A few rocks on the bottom — proof the refracted view resolves real
// geometry (position, tint shift with depth), plus one breaking the
// surface as an above-water reference.
auto rockMat = MeshStandardMaterial::create(
MeshStandardMaterial::Params{}.color(Color(0.42f, 0.38f, 0.33f)).roughness(0.9f));
const struct { float x, z, r, y; } rocks[] = {
{-2.5f, -1.5f, 0.55f, -1.05f}, {1.8f, 0.8f, 0.75f, -0.95f},
{0.2f, -3.0f, 0.4f, -1.1f}, {3.4f, -2.2f, 0.5f, -1.0f},
{-1.2f, 2.6f, 1.05f, -0.35f}// last one clearly pierces the surface
};
for (const auto& r : rocks) {
auto rock = Mesh::create(SphereGeometry::create(r.r, 24, 18), rockMat);
rock->position.set(r.x, r.y, r.z);
rock->scale.y = 0.75f;// flattened boulders
scene.add(rock);
}
}
// The whole "fancy water" in one line. For the pond, size alone retunes
// the cascades (auto tiles); calm default wind unless overridden.
Ocean::Options opts;
opts.size = kSize;
if (pond) opts.windSpeed = 3.0f;
if (windOverride >= 0.f) opts.windSpeed = windOverride;
auto ocean = Ocean::create(opts);
scene.add(ocean);
// Live wind control — setWind() takes effect next frame; the sea morphs
// smoothly because the spectrum noise is persistent.
KeyAdapter windKeys(KeyAdapter::Mode::KEY_PRESSED, [&](KeyEvent ev) {
auto& p = ocean->params;
float speed = p.windSpeed, theta = p.windTheta;
if (ev.key == Key::UP) speed = std::min(speed + 1.f, 30.f);
else if (ev.key == Key::DOWN) speed = std::max(speed - 1.f, 0.5f);
else if (ev.key == Key::LEFT) theta -= math::PI / 12.f;
else if (ev.key == Key::RIGHT) theta += math::PI / 12.f;
else return;
ocean->setWind(speed, theta);
std::cout << "wind: " << speed << " m/s, "
<< theta * math::RAD2DEG << " deg" << std::endl;
});
canvas.addKeyListener(windKeys);
PerspectiveCamera camera(50.f, canvas.aspect(), 0.1f, 2000.f);
if (pond) camera.position.set(0.f, 2.4f, 8.5f);
else camera.position.set(0.f, 10.f, 35.f);
if (capArgs.camPos) camera.position.copy(*capArgs.camPos);
OrbitControls controls{camera, canvas};
controls.target.set(0.f, 0.f, 0.f);
if (capArgs.camTarget) controls.target.copy(*capArgs.camTarget);
controls.update();
canvas.onWindowResize([&](const WindowSize& ns) {
renderer.setSize(ns);
camera.aspect = canvas.aspect();
camera.updateProjectionMatrix();
});
// sampleHeight parity probes (--probes): emissive spheres pinned to the
// CPU-mirrored wave height each frame. If the CPU sampler matches the GPU
// displacement they sit ON the surface; any axis flip / offset shows as
// spheres hovering or submerged. Also exercises the lazy height readback.
std::vector<std::shared_ptr<Mesh>> probeSpheres;
if (probes) {
auto probeMat = MeshStandardMaterial::create(MeshStandardMaterial::Params{}
.color(Color(1.f, 0.15f, 0.05f)).roughness(0.4f));
probeMat->emissive = Color(1.f, 0.2f, 0.05f);
probeMat->emissiveIntensity = 2.f;
for (int i = 0; i < 9; ++i) {
auto s = Mesh::create(SphereGeometry::create(pond ? 0.12f : 0.5f, 16, 12), probeMat);
probeSpheres.push_back(s);
scene.add(s);
}
}
canvas.animate([&] {
controls.update();
// Pack vertex density toward where the camera is looking. The focus is
// just a world coordinate — the same warp the showcase points at a boat.
ocean->warpToward(controls.target.x, controls.target.z, 0.3f);
if (wind2 >= 0.f && shotFrame == shotFrames / 2)
ocean->setWind(wind2, ocean->params.windTheta);
if (!probeSpheres.empty()) {
const float spacing = pond ? 3.f : 12.f;
int k = 0;
for (int gz = -1; gz <= 1; ++gz)
for (int gx = -1; gx <= 1; ++gx) {
const float px = controls.target.x + float(gx) * spacing;
const float pz = controls.target.z + float(gz) * spacing;
probeSpheres[k++]->position.set(
px, ocean->sampleHeight(px, pz), pz);
}
}
renderer.render(scene, camera);
// --bench: after the warm-up (probe GI convergence, pipeline warm,
// BLAS refit cadence settling), collect lastFrameTimings() per frame
// and report median + mean per bracketed pass. The brackets open at
// TOP_OF_PIPE and close at ALL_COMMANDS, so back-to-back passes can
// overlap slightly — read each column as an upper bound on that pass
// alone, and the residual as a floor on the unbracketed work.
if (benchFrames > 0) {
static int benchTick = 0;
static std::vector<VulkanRenderer::FrameTimings> samples;
constexpr int kBenchWarmup = 240;
if (++benchTick > kBenchWarmup) {
samples.push_back(renderer.lastFrameTimings());
if (static_cast<int>(samples.size()) >= benchFrames) {
auto stat = [&](const char* name, auto get) {
std::vector<float> v;
v.reserve(samples.size());
for (const auto& t : samples) v.push_back(get(t));
std::sort(v.begin(), v.end());
double sum = 0;
for (float x : v) sum += x;
std::printf(" %-14s median %7.3f ms mean %7.3f ms\n",
name, v[v.size() / 2], sum / double(v.size()));
};
using FT = VulkanRenderer::FrameTimings;
std::printf("ocean bench: %zu samples after %d warm-up frames, %s, wind %.1f m/s\n",
samples.size(), kBenchWarmup, pond ? "pond" : "ocean",
ocean->params.windSpeed);
stat("oceanFft", [](const FT& t) { return t.oceanFftMs; });
stat("oceanDisplace", [](const FT& t) { return t.oceanDisplaceMs; });
stat("oceanFoam", [](const FT& t) { return t.oceanFoamMs; });
stat("oceanBlas", [](const FT& t) { return t.oceanBlasMs; });
stat("tlasRefit", [](const FT& t) { return t.tlasRefitMs; });
stat("rasterGbuf", [](const FT& t) { return t.rasterGbufMs; });
stat("deferredShade", [](const FT& t) { return t.pathTraceMs; });
stat("denoise", [](const FT& t) { return t.denoiseMs; });
stat("taa", [](const FT& t) { return t.taaMs; });
stat("gpuPassSum", [](const FT& t) { return t.gpuPassSumMs; });
stat("gpuTotal", [](const FT& t) { return t.gpuTotalMs; });
stat("gpuResidual", [](const FT& t) { return t.gpuTotalMs - t.gpuPassSumMs; });
stat("cpuRecord", [](const FT& t) { return t.cpuRecordMs; });
stat("cpuFrame", [](const FT& t) { return t.cpuFrameMs; });
std::exit(0);
}
}
}
if (!shotPath.empty() && ++shotFrame >= shotFrames) {
const auto path = std::filesystem::path(PROJECT_FOLDER) / "aaa_caps" / shotPath;
std::filesystem::create_directories(path.parent_path());
renderer.writeFramebuffer(path);
std::cout << "wrote " << path.string() << std::endl;
std::exit(0);
}
});
return 0;
}