@photivra/engine 2.9.0 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +223 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +21 -3
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +2 -0
- package/dist/capture/native-environment-lean.js +89 -5
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-tabulated.d.ts +37 -0
- package/dist/capture/native-environment-tabulated.js +42 -6
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/core/portable-math.d.ts +19 -0
- package/dist/core/portable-math.js +82 -0
- package/dist/core/portable-math.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/channel-response.d.ts +15 -0
- package/dist/facade/channel-response.js +33 -0
- package/dist/facade/channel-response.js.map +1 -0
- package/dist/facade/chart-material-layers.d.ts +115 -0
- package/dist/facade/chart-material-layers.js +224 -0
- package/dist/facade/chart-material-layers.js.map +1 -0
- package/dist/facade/chart-radiance-batch.d.ts +8 -2
- package/dist/facade/chart-radiance-batch.js +11 -4
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/chart-scenes.d.ts +134 -0
- package/dist/facade/chart-scenes.js +32 -22
- package/dist/facade/chart-scenes.js.map +1 -1
- package/dist/facade/diffraction-provider.d.ts +8 -2
- package/dist/facade/diffraction-provider.js +120 -16
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.d.ts +18 -0
- package/dist/facade/display-view-gpu-session.js +280 -0
- package/dist/facade/display-view-gpu-session.js.map +1 -0
- package/dist/facade/display-view-gpu.d.ts +146 -0
- package/dist/facade/display-view-gpu.js +59 -0
- package/dist/facade/display-view-gpu.js.map +1 -0
- package/dist/facade/display-view.d.ts +123 -1
- package/dist/facade/display-view.js +478 -116
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +5 -0
- package/dist/facade/expected-signal-setup.js +18 -1
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +80 -6
- package/dist/facade/expected-signal.js +200 -24
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/scene-radiance.d.ts +4 -2
- package/dist/facade/scene-radiance.js +7 -4
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +81 -7
- package/dist/facade/simulate-capture.js +276 -53
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +54 -3
- package/dist/facade/simulate-display-view.js +120 -27
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.d.ts +119 -0
- package/dist/gpu/chart-composite-gpu.js +572 -0
- package/dist/gpu/chart-composite-gpu.js.map +1 -0
- package/dist/gpu/chart-radiance-gpu.d.ts +6 -6
- package/dist/gpu/chart-radiance-gpu.js +9 -11
- package/dist/gpu/chart-radiance-gpu.js.map +1 -1
- package/dist/gpu/display-view-chart-material-module.d.ts +10 -0
- package/dist/gpu/display-view-chart-material-module.js +58 -0
- package/dist/gpu/display-view-chart-material-module.js.map +1 -0
- package/dist/gpu/display-view-composite-material-module.d.ts +68 -0
- package/dist/gpu/display-view-composite-material-module.js +248 -0
- package/dist/gpu/display-view-composite-material-module.js.map +1 -0
- package/dist/gpu/display-view-quality-gpu.d.ts +185 -0
- package/dist/gpu/display-view-quality-gpu.js +1000 -0
- package/dist/gpu/display-view-quality-gpu.js.map +1 -0
- package/dist/gpu/display-view-speed-gpu.d.ts +156 -0
- package/dist/gpu/display-view-speed-gpu.js +524 -0
- package/dist/gpu/display-view-speed-gpu.js.map +1 -0
- package/dist/gpu/image-kernel-accelerator.d.ts +23 -4
- package/dist/gpu/image-kernel-accelerator.js +58 -10
- package/dist/gpu/image-kernel-accelerator.js.map +1 -1
- package/dist/gpu/image-kernel-execution.d.ts +60 -3
- package/dist/gpu/image-kernel-execution.js +2 -2
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/gpu/sensor-stage-gpu.d.ts +11 -0
- package/dist/gpu/sensor-stage-gpu.js +42 -13
- package/dist/gpu/sensor-stage-gpu.js.map +1 -1
- package/dist/gpu/webgpu-runtime.d.ts +4 -2
- package/dist/gpu/webgpu-runtime.js +13 -4
- package/dist/gpu/webgpu-runtime.js.map +1 -1
- package/dist/gpu/webgpu-switch.d.ts +4 -0
- package/dist/gpu/webgpu-switch.js +29 -0
- package/dist/gpu/webgpu-switch.js.map +1 -0
- package/dist/gpu/wgsl-chart-composite-kernel.d.ts +52 -0
- package/dist/gpu/wgsl-chart-composite-kernel.js +515 -0
- package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -0
- package/dist/gpu/wgsl-chart-kernel.d.ts +11 -9
- package/dist/gpu/wgsl-chart-kernel.js +33 -27
- package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +95 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.js +769 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +75 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.js +386 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -0
- package/dist/gpu/wgsl-sensor-kernel.d.ts +28 -9
- package/dist/gpu/wgsl-sensor-kernel.js +94 -50
- package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/optics/defocus-disc-kernel.d.ts +61 -0
- package/dist/optics/defocus-disc-kernel.js +191 -0
- package/dist/optics/defocus-disc-kernel.js.map +1 -0
- package/dist/optics/irradiance-convolution.d.ts +11 -0
- package/dist/optics/irradiance-convolution.js +64 -24
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
- package/dist/optics/scene-to-sensor-irradiance.js +58 -24
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/optics/sensor-aperture-rays.js +2 -1
- package/dist/optics/sensor-aperture-rays.js.map +1 -1
- package/dist/optics/sensor-environment-query.js +4 -3
- package/dist/optics/sensor-environment-query.js.map +1 -1
- package/dist/optics/site-diffraction-kernel.d.ts +16 -0
- package/dist/optics/site-diffraction-kernel.js +60 -8
- package/dist/optics/site-diffraction-kernel.js.map +1 -1
- package/dist/processing/demosaic.js +114 -37
- package/dist/processing/demosaic.js.map +1 -1
- package/dist/schema/scene-radiance.d.ts +8 -0
- package/dist/schema/scene-radiance.js +28 -17
- package/dist/schema/scene-radiance.js.map +1 -1
- package/dist/sensor/environment-photo-signal.d.ts +21 -0
- package/dist/sensor/environment-photo-signal.js +68 -2
- package/dist/sensor/environment-photo-signal.js.map +1 -1
- package/docs/API_REFERENCE.md +60 -1
- package/docs/QUICK_START.md +120 -11
- package/docs/USAGE.md +82 -2
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +157 -2
- package/docs/api/facade-chart-material-layers.md +137 -0
- package/docs/api/facade-chart-radiance-batch.md +7 -2
- package/docs/api/facade-display-view-gpu.md +94 -0
- package/docs/api/facade-display-view.md +50 -0
- package/docs/api/facade-scene-radiance.md +3 -2
- package/docs/api/facade-simulate-capture.md +81 -12
- package/docs/api/facade-simulate-display-view.md +39 -2
- package/docs/api/gpu-image-kernel-accelerator.md +10 -3
- package/docs/api/gpu-image-kernel-execution.md +76 -4
- package/docs/api/gpu-webgpu-switch.md +23 -0
- package/package.json +3 -1
|
@@ -72,9 +72,11 @@ import { createRollingShutterCaptureExposureSchedule, resolveElectronicRollingSh
|
|
|
72
72
|
import { calculateCaptureExposureWindows } from "../sensor/exposure-window.js";
|
|
73
73
|
import { calculateSensorSpectralQuadrature } from "../sensor/spectral-quadrature.js";
|
|
74
74
|
import { createDiffractionLimitedPsfProfile } from "../optics/diffraction-psf-profile.js";
|
|
75
|
+
import { registerDiffractionPsfKey, siteDiffractionKernel } from "../optics/site-diffraction-kernel.js";
|
|
76
|
+
import { convolveSiteKernels, createDefocusDiscSiteKernel, requireDefocusDiscKernelRadius, resolveDefocusDisc } from "../optics/defocus-disc-kernel.js";
|
|
75
77
|
import { createDiffractedPlanarProvider } from "./diffraction-provider.js";
|
|
76
78
|
import { resolveGenericPrimeLens } from "../equipment/generic-prime-lenses.js";
|
|
77
|
-
import {
|
|
79
|
+
import { calculateChannelElectronsPerRadiance } from "./channel-response.js";
|
|
78
80
|
import { calculateSiteEffectiveSaturationCode } from "../capture/raw-white-level.js";
|
|
79
81
|
import { prepareSensorColorDevelopment } from "../color/sensor-color-development.js";
|
|
80
82
|
import { resolveAntiAliasingSpatialKernel } from "../sensor/optical-stack.js";
|
|
@@ -83,6 +85,7 @@ import { prepareRgbGamutMapping } from "../color/gamut-mapping.js";
|
|
|
83
85
|
import { serializeNativeRawDng } from "../capture/native-raw-dng.js";
|
|
84
86
|
import { parseSensorReadoutConversionProfile, resolveSensorReadoutRegime } from "../sensor/raw-readout.js";
|
|
85
87
|
import { resolveChartScene, chartMaterialInputs, CHART_SCENE_PRESETS } from "./chart-scenes.js";
|
|
88
|
+
import { resolveChartMaterialLayers } from "./chart-material-layers.js";
|
|
86
89
|
import { resolveCaptureIlluminant, calculateIlluminantIrradianceScale, buildPlanarSceneRadianceProvider, requireCallerScene, FACADE_SPECTRAL_RANGE_NANOMETERS, resolveChartSizeOption, PHOTOPIC_MAXIMUM_LUMINOUS_EFFICACY_LM_PER_W, planarProviderMaterialOfDirection } from "./scene-radiance.js";
|
|
87
90
|
/** Version of how the facade builds requests. It changes whenever the same input would build a different request. */
|
|
88
91
|
export const SIMULATE_CAPTURE_FACADE_VERSION = "1.0.0";
|
|
@@ -93,9 +96,15 @@ export const SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES = 4;
|
|
|
93
96
|
* a capture does. `motion.rotation` is a non-zero angular velocity; `sensor.temporalSampleCount` a count above 1.
|
|
94
97
|
*/
|
|
95
98
|
export const SIMULATE_CAPTURE_COST_OPTIONS = Object.freeze(["lens.pupilSampling", "lens.projection", "lens.distortion", "lens.vignetting", "lens.psf",
|
|
96
|
-
"lens.diffraction", "sensor.antiAliasing", "sensor.pixelAperture.box", "sensor.temporalSampleCount", "motion.rotation", "motion.shutter.rolling", "output.borderHandling.mirror", "output.region",
|
|
99
|
+
"lens.diffraction", "lens.defocus", "sensor.antiAliasing", "sensor.pixelAperture.box", "sensor.temporalSampleCount", "motion.rotation", "motion.shutter.rolling", "output.borderHandling.mirror", "output.region",
|
|
97
100
|
"output.whiteBalance.auto", "output.demosaic.ahd", "output.highlightMode.reconstruct", "output.colorEncoding.wide-gamut", "output.gamutMapping.perceptual",
|
|
98
101
|
"output.format.jpeg", "output.format.png", "output.format.dng"]);
|
|
102
|
+
/**
|
|
103
|
+
* The options that take the exact-or-fast flag (#395), in the path style of {@link SIMULATE_CAPTURE_COST_OPTIONS}: each accepts
|
|
104
|
+
* the record form with `fast` and is reported in `diagnostics.fast.applied` when it runs. Every one of them takes the flag,
|
|
105
|
+
* whether or not it has a fast method yet; one without runs exact and reports `false`.
|
|
106
|
+
*/
|
|
107
|
+
export const SIMULATE_CAPTURE_FAST_OPTIONS = Object.freeze(["lens.diffraction", "sensor.antiAliasing", "sensor.pixelAperture.box", "output.demosaic.ahd"]);
|
|
99
108
|
export const FACADE_EVIDENCE = Object.freeze([Object.freeze({ sourceOrigin: "photivra",
|
|
100
109
|
sourceReference: "photivra:simulate-capture-facade", reuseStatus: "photivra-owned" })]);
|
|
101
110
|
const DEFAULTS = Object.freeze({ camera: "professional", fNumber: 8, shutterSeconds: 1 / 125, iso: 100, compensationStops: 0,
|
|
@@ -145,8 +154,52 @@ const AA_MAXIMUM_OFFSET_SITES = 4;
|
|
|
145
154
|
/** Channel responses kept across calls (`axisChannelResponse`): one entry per distinct sensor, exposure and optics state. */
|
|
146
155
|
const CHANNEL_RESPONSE_CACHE_SIZE = 8;
|
|
147
156
|
const channelResponseCache = new Map();
|
|
157
|
+
/**
|
|
158
|
+
* #449: diffraction profiles kept across calls, by the JSON of their `createDiffractionLimitedPsfProfile` input (the last
|
|
159
|
+
* DIFFRACTION_PROFILE_CACHE_SIZE, most recently used last). Every region part and detail tile of one request builds the same
|
|
160
|
+
* input. The kept record is private: each call gets its own copy, as mutable as a newly built one, so a caller's change
|
|
161
|
+
* never reaches another call. The copy is made of plain records and arrays (plainDataCopy): the kernel set-up structured-clones
|
|
162
|
+
* the profile once per spectral node, and in a CPU profile of the 96 × 64 Quality tile that clone took about twice as long
|
|
163
|
+
* (24 instead of 12 ms per call) on a deep-frozen record or on a structuredClone copy as on a newly built record or this copy.
|
|
164
|
+
*/
|
|
165
|
+
const DIFFRACTION_PROFILE_CACHE_SIZE = 8;
|
|
166
|
+
const diffractionProfileCache = new Map();
|
|
167
|
+
/** A copy of plain data (records, arrays, primitives; typed arrays are sliced), built from ordinary literals. */
|
|
168
|
+
function plainDataCopy(value) {
|
|
169
|
+
if (Array.isArray(value))
|
|
170
|
+
return value.map(plainDataCopy);
|
|
171
|
+
if (ArrayBuffer.isView(value))
|
|
172
|
+
return value.slice();
|
|
173
|
+
if (value === null || typeof value !== "object")
|
|
174
|
+
return value;
|
|
175
|
+
const copy = {};
|
|
176
|
+
for (const key of Object.keys(value))
|
|
177
|
+
copy[key] = plainDataCopy(value[key]);
|
|
178
|
+
return copy;
|
|
179
|
+
}
|
|
180
|
+
/** The profile of `input` (a pure function of the input record): a copy of the kept one when the same input was built before. */
|
|
181
|
+
function cachedDiffractionProfile(input) {
|
|
182
|
+
const key = JSON.stringify(input);
|
|
183
|
+
let kept = diffractionProfileCache.get(key);
|
|
184
|
+
if (kept)
|
|
185
|
+
diffractionProfileCache.delete(key);
|
|
186
|
+
else
|
|
187
|
+
kept = createDiffractionLimitedPsfProfile(input).value;
|
|
188
|
+
diffractionProfileCache.set(key, kept);
|
|
189
|
+
if (diffractionProfileCache.size > DIFFRACTION_PROFILE_CACHE_SIZE)
|
|
190
|
+
diffractionProfileCache.delete(diffractionProfileCache.keys().next().value);
|
|
191
|
+
// The same fields in the same order as the built record, frozen at the top level like it; `model` is the frozen constant.
|
|
192
|
+
return Object.freeze({ ...kept, profile: plainDataCopy(kept.profile), wavelengths: plainDataCopy(kept.wavelengths), quadrature: plainDataCopy(kept.quadrature) });
|
|
193
|
+
}
|
|
148
194
|
/** Default birefringent separation in sites: the two-spot MTF |cos(π·f·d)| then has its first zero at Nyquist, f = 1/(2·pitch). */
|
|
149
195
|
const AA_DEFAULT_SEPARATION_SITES = 1;
|
|
196
|
+
/** #395: the `fast` override of an option record (undefined when absent), which must be a Boolean. */
|
|
197
|
+
function fastOverride(record, path) {
|
|
198
|
+
const value = record.fast;
|
|
199
|
+
if (value !== undefined && typeof value !== "boolean")
|
|
200
|
+
fail(path + ".fast must be true or false.");
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
150
203
|
/** Parses `sensor.antiAliasing` into engine point-splitting components (offsets in µm, +x right, +y down). */
|
|
151
204
|
function parseAntiAliasing(value, pitch) {
|
|
152
205
|
requireInputObject(value, "sensor.antiAliasing");
|
|
@@ -160,18 +213,18 @@ function parseAntiAliasing(value, pitch) {
|
|
|
160
213
|
return d;
|
|
161
214
|
};
|
|
162
215
|
if (kind === "four-spot") {
|
|
163
|
-
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "separationSites"]), d = separation(record), h = d * pitch / 2;
|
|
216
|
+
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "separationSites", "fast"]), d = separation(record), h = d * pitch / 2;
|
|
164
217
|
return { kind, separationSites: d, components: [[-h, -h], [h, -h], [-h, h], [h, h]].map(([x, y]) => ({ offsetMicrometers: { x: x, y: y }, normalizedWeight: 0.25 })) };
|
|
165
218
|
}
|
|
166
219
|
if (kind === "two-spot") {
|
|
167
|
-
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "axis", "separationSites"]), d = separation(record), h = d * pitch / 2;
|
|
220
|
+
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "axis", "separationSites", "fast"]), d = separation(record), h = d * pitch / 2;
|
|
168
221
|
if (record.axis !== "horizontal" && record.axis !== "vertical")
|
|
169
222
|
fail("sensor.antiAliasing.axis must be horizontal or vertical.");
|
|
170
223
|
const axis = record.axis;
|
|
171
224
|
return { kind, axis, separationSites: d, components: [-h, h].map((o) => ({ offsetMicrometers: axis === "horizontal" ? { x: o, y: 0 } : { x: 0, y: o }, normalizedWeight: 0.5 })) };
|
|
172
225
|
}
|
|
173
226
|
if (kind === "point-splitting") {
|
|
174
|
-
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "components"]);
|
|
227
|
+
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "components", "fast"]);
|
|
175
228
|
if (!Array.isArray(record.components) || record.components.length === 0 || record.components.length > AA_MAXIMUM_COMPONENTS) {
|
|
176
229
|
fail("sensor.antiAliasing.components must be an array of 1 to " + AA_MAXIMUM_COMPONENTS + " components.");
|
|
177
230
|
}
|
|
@@ -283,11 +336,25 @@ function spectrumDigestForm(raw, prepared) {
|
|
|
283
336
|
values.push(prepared.evaluate(nm));
|
|
284
337
|
return { kind: "function-sampled-1nm", rangeNanometers: [FACADE_SPECTRAL_RANGE_NANOMETERS.minimum, FACADE_SPECTRAL_RANGE_NANOMETERS.maximum], values };
|
|
285
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* #449: the CIE 1931 ȳ values at luminanceOf's 5 nm nodes, evaluated once (estimateCie1931ColorMatchingFunctions is a pure
|
|
341
|
+
* function of the wavelength, so each entry is the value the loop would compute).
|
|
342
|
+
*/
|
|
343
|
+
let luminanceCmfY;
|
|
344
|
+
function luminanceNodesY() {
|
|
345
|
+
if (luminanceCmfY)
|
|
346
|
+
return luminanceCmfY;
|
|
347
|
+
const values = [];
|
|
348
|
+
for (let nm = FACADE_SPECTRAL_RANGE_NANOMETERS.minimum; nm <= FACADE_SPECTRAL_RANGE_NANOMETERS.maximum; nm += 5)
|
|
349
|
+
values.push(estimateCie1931ColorMatchingFunctions(nm).y);
|
|
350
|
+
return (luminanceCmfY = Float64Array.from(values));
|
|
351
|
+
}
|
|
286
352
|
function luminanceOf(radiance) {
|
|
353
|
+
const cmfY = luminanceNodesY();
|
|
287
354
|
let sum = 0;
|
|
288
|
-
for (let nm = FACADE_SPECTRAL_RANGE_NANOMETERS.minimum; nm <= FACADE_SPECTRAL_RANGE_NANOMETERS.maximum; nm += 5) {
|
|
355
|
+
for (let nm = FACADE_SPECTRAL_RANGE_NANOMETERS.minimum, i = 0; nm <= FACADE_SPECTRAL_RANGE_NANOMETERS.maximum; nm += 5, i++) {
|
|
289
356
|
const w = nm === FACADE_SPECTRAL_RANGE_NANOMETERS.minimum || nm === FACADE_SPECTRAL_RANGE_NANOMETERS.maximum ? 2.5 : 5;
|
|
290
|
-
sum += w * radiance(nm) *
|
|
357
|
+
sum += w * radiance(nm) * cmfY[i];
|
|
291
358
|
}
|
|
292
359
|
return PHOTOPIC_MAXIMUM_LUMINOUS_EFFICACY_LM_PER_W * sum;
|
|
293
360
|
}
|
|
@@ -600,6 +667,53 @@ export function parseDisplayViewOption(value, frameWidth, frameHeight) {
|
|
|
600
667
|
}
|
|
601
668
|
return { width, height, samplesPerAxis, bitDepth: format === "srgb-8" ? 8 : 16, format: format, ...(region ? { region } : {}) };
|
|
602
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* `lens.defocus` (#492): resolves the geometric defocus disc at the resolved f-number on the site pitch and records it.
|
|
672
|
+
* In focus (c = 0) no kernel exists and nothing else changes (undefined). The capture (step 2a) applies the disc as an
|
|
673
|
+
* object-space convolution and the display view (step 2b) as a site kernel in its expected-signal tables, and both get it
|
|
674
|
+
* back; the ideal reference, which has no camera effects, only records it.
|
|
675
|
+
*/
|
|
676
|
+
function resolveDefocusOption(input) {
|
|
677
|
+
const { run, assume, focalLengthMm, fNumber, focusDistanceM, subjectDistanceM, pitch } = input;
|
|
678
|
+
const disc = resolveDefocusDisc({ focalLengthMm, aperture: fNumber, focusDistanceM, subjectDistanceM, pitchMicrometers: pitch });
|
|
679
|
+
const size = disc.diameterMicrometers + " µm (" + disc.diameterSites + " sites of " + pitch + " µm)";
|
|
680
|
+
let radius;
|
|
681
|
+
try {
|
|
682
|
+
radius = requireDefocusDiscKernelRadius(disc.diameterSites);
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
fail("lens.defocus: the blur circle is " + size + ". " + error.message);
|
|
686
|
+
}
|
|
687
|
+
const value = { model: "geometric-disc", scientificStatus: "approximation", diameterMicrometers: disc.diameterMicrometers, diameterSites: disc.diameterSites,
|
|
688
|
+
kernelRadiusSites: disc.diameterSites === 0 ? null : radius, fNumber, focusDistanceM, subjectDistanceM, samplePitchMicrometers: pitch,
|
|
689
|
+
defocusCircleModel: { id: "thin-lens-defocus-circle", version: "1.1.0" } };
|
|
690
|
+
const model = "Geometric defocus of the flat scene plane (an approximation): the thin-lens blur circle of an ideal uniform circular pupil, " +
|
|
691
|
+
"c = f²·|s − d| / (N·d·(s − f)) (calculateDefocusCircle 1.1.0), as a uniform disc sampled on the site lattice (exact disc area in each site's cell). " +
|
|
692
|
+
"No diffraction of the defocused wave, aberrations or pupil vignetting. ";
|
|
693
|
+
if (disc.diameterSites === 0) {
|
|
694
|
+
assume("lens.defocus", "model", value, model + "In focus (lens.focusDistanceM equals subjectDistanceM): the disc has zero diameter, so no defocus kernel is applied.");
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
if (run === "ideal-reference") {
|
|
698
|
+
assume("lens.defocus", "model", value, model + "Resolved but not applied: the ideal reference has no camera effects.");
|
|
699
|
+
return undefined;
|
|
700
|
+
}
|
|
701
|
+
// The display view (#492, step 2b): a site kernel in its expected-signal tables (simulate-display-view.ts).
|
|
702
|
+
if (run === "display-view") {
|
|
703
|
+
assume("lens.defocus", "model", { ...value, execution: "expected-signal-site-kernel", combinedWithDiffraction: input.diffraction }, model +
|
|
704
|
+
"Applied in the display view's expected-signal tables as a site kernel on the lattice" + (input.diffraction ? " (with lens.diffraction, each spectral node's " +
|
|
705
|
+
"diffraction kernel convolved with the disc, a further approximation)" : "") + "; see the view.defocus assumption.");
|
|
706
|
+
return disc;
|
|
707
|
+
}
|
|
708
|
+
// The capture (#492, step 2a): an object-space convolution of the scene radiance on the site lattice.
|
|
709
|
+
assume("lens.defocus", "model", { ...value, execution: "object-space-convolution", combinedWithDiffraction: input.diffraction }, model +
|
|
710
|
+
"Applied as an object-space convolution: the scene radiance on the site lattice is convolved with the disc, the same kernel at every spectral node, " +
|
|
711
|
+
"and each site query is answered with the result (for a flat scene and a still, distortion-free rectilinear camera every disc point a site sees is the scene point of another site). " +
|
|
712
|
+
(input.diffraction ? "With lens.diffraction, each spectral node's diffraction kernel is convolved with the disc and applied as one kernel. That is a further approximation: " +
|
|
713
|
+
"the defocused wave PSF of a circular pupil is not exactly the in-focus Airy pattern convolved with the geometric disc. " : "") +
|
|
714
|
+
"The disc kernel is a sampling of the disc on the site lattice; lens.pupilSampling (ray-sampled, no diffraction) is the independent reference.");
|
|
715
|
+
return disc;
|
|
716
|
+
}
|
|
603
717
|
/** Internal: passed as the view of a "display-view" run to resolve the request without a view (`prepareDisplayView`, #387). */
|
|
604
718
|
export const DISPLAY_VIEW_WITHOUT_VIEW = Symbol("display view without view");
|
|
605
719
|
/**
|
|
@@ -610,7 +724,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
610
724
|
const started = Date.now(), idealReference = run === "ideal-reference", displayView = run === "display-view";
|
|
611
725
|
requireInputObject(options, "simulateCapture options");
|
|
612
726
|
const top = allowKeys(options, "simulateCapture options", ["camera", "lens", "exposure", "scene", "subjectDistanceM", "chartSize", "illuminant",
|
|
613
|
-
"illuminanceLux", "output", "sensor", "motion", "signal", "onProgress"]);
|
|
727
|
+
"illuminanceLux", "output", "sensor", "motion", "fast", "signal", "onProgress"]);
|
|
614
728
|
const signal = top.signal, onProgress = top.onProgress;
|
|
615
729
|
if (signal !== undefined && (typeof signal !== "object" || signal === null || typeof signal.aborted !== "boolean"))
|
|
616
730
|
fail("signal must be an AbortSignal.");
|
|
@@ -636,7 +750,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
636
750
|
preset = parseCallerCameraPreset(cameraOption);
|
|
637
751
|
const body = preset.body, lens = preset.lens;
|
|
638
752
|
// Lens
|
|
639
|
-
const lensOptions = allowKeys(top.lens, "lens", ["focalLengthMm", "fNumber", "focusDistanceM", "pupilSampling", "projection", "distortion", "vignetting", "psf", "diffraction", "prime"]);
|
|
753
|
+
const lensOptions = allowKeys(top.lens, "lens", ["focalLengthMm", "fNumber", "focusDistanceM", "pupilSampling", "projection", "distortion", "vignetting", "psf", "diffraction", "prime", "defocus"]);
|
|
640
754
|
// Generic prime (2.3): resolved on the simulated frame's size, which the output and sensor sections validate again below.
|
|
641
755
|
let prime;
|
|
642
756
|
if (lensOptions.prime !== undefined) {
|
|
@@ -669,7 +783,9 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
669
783
|
fail("The " + prime.lensId + " focuses no closer than " + prime.minimumFocusDistanceM + " m (max(0.10 m, f·(1 + 1/0.15))); subjectDistanceM and lens.focusDistanceM must be at least that.");
|
|
670
784
|
let pupil;
|
|
671
785
|
if (lensOptions.pupilSampling === undefined)
|
|
672
|
-
assume("lens.pupilSampling", "default", null,
|
|
786
|
+
assume("lens.pupilSampling", "default", null, lensOptions.defocus === undefined ?
|
|
787
|
+
"No pupil sampling: each scene query follows the central (chief) ray, so defocus and depth of field are not rendered. Enable lens.pupilSampling for an ideal circular pupil." :
|
|
788
|
+
"No pupil sampling: each scene query follows the central (chief) ray; defocus of the scene plane is the lens.defocus option's (see the lens.defocus assumption).");
|
|
673
789
|
else {
|
|
674
790
|
const p = allowKeys(lensOptions.pupilSampling, "lens.pupilSampling", ["radialSamples", "angularSamples"]);
|
|
675
791
|
// The environment pupil contract admits 4..64 azimuthal midpoints and at most 256 rays.
|
|
@@ -714,22 +830,39 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
714
830
|
lensPsf = p;
|
|
715
831
|
}
|
|
716
832
|
// Diffraction (2.1): validated here, built once the f-number is resolved.
|
|
717
|
-
let diffractionRadiusOption;
|
|
833
|
+
let diffractionRadiusOption, diffractionFast;
|
|
718
834
|
if (lensOptions.diffraction !== undefined) {
|
|
719
835
|
const d = lensOptions.diffraction;
|
|
720
836
|
if (d === "circular-aperture")
|
|
721
837
|
diffractionRadiusOption = null;
|
|
722
838
|
else {
|
|
723
|
-
const record = allowKeys(d, "lens.diffraction", ["aperture", "kernelRadiusSamples"]);
|
|
724
|
-
|
|
725
|
-
|
|
839
|
+
const record = allowKeys(d, "lens.diffraction", ["kind", "aperture", "kernelRadiusSamples", "fast"]);
|
|
840
|
+
// #395: the record names the model by `kind`; `aperture` is its deprecated alias (until 3.0.0).
|
|
841
|
+
if (record.kind !== undefined && record.aperture !== undefined)
|
|
842
|
+
fail("lens.diffraction takes kind or its deprecated alias aperture, not both.");
|
|
843
|
+
const model = record.kind !== undefined ? record.kind : record.aperture;
|
|
844
|
+
if (model !== "circular-aperture")
|
|
845
|
+
fail("lens.diffraction must be \"circular-aperture\" or { kind: \"circular-aperture\", kernelRadiusSamples, fast }.");
|
|
726
846
|
diffractionRadiusOption = record.kernelRadiusSamples === undefined ? null : integerIn(record.kernelRadiusSamples, "lens.diffraction.kernelRadiusSamples", 0, 32);
|
|
847
|
+
diffractionFast = fastOverride(record, "lens.diffraction");
|
|
727
848
|
}
|
|
728
849
|
if (lensPsf)
|
|
729
850
|
fail("lens.diffraction cannot be combined with lens.psf: diffraction would be counted twice.");
|
|
730
851
|
if (pupil)
|
|
731
852
|
fail("lens.diffraction cannot be combined with lens.pupilSampling: the environment contract does not mix a sampled PSF with pupil rays.");
|
|
732
853
|
}
|
|
854
|
+
// Defocus (#492): validated here, resolved once the f-number is.
|
|
855
|
+
let defocusModel;
|
|
856
|
+
if (lensOptions.defocus !== undefined) {
|
|
857
|
+
const record = allowKeys(lensOptions.defocus, "lens.defocus", ["model"]);
|
|
858
|
+
if (record.model !== "geometric-disc")
|
|
859
|
+
fail("lens.defocus.model must be \"geometric-disc\".");
|
|
860
|
+
if (pupil)
|
|
861
|
+
fail("lens.defocus cannot be combined with lens.pupilSampling: the pupil rays already render defocus, so it would be counted twice.");
|
|
862
|
+
if (lensPsf)
|
|
863
|
+
fail("lens.defocus cannot be combined with lens.psf: a caller PSF declares its own defocus, so it could be counted twice.");
|
|
864
|
+
defocusModel = "geometric-disc";
|
|
865
|
+
}
|
|
733
866
|
// Motion (2.0): constant camera rotation and the shutter type.
|
|
734
867
|
const motionOptions = allowKeys(top.motion, "motion", ["angularVelocityRadPerSec", "shutter", "rollingShutterSkewSeconds"]);
|
|
735
868
|
const angular = { pitch: 0, yaw: 0, roll: 0 };
|
|
@@ -824,16 +957,18 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
824
957
|
pick(sensorOptions, "temporalSampleCount", "sensor.temporalSampleCount", DEFAULTS.temporalSampleCount, "One temporal sample: the static scene and still camera make more samples unnecessary."), "sensor.temporalSampleCount", 1, 256);
|
|
825
958
|
const noiseSeed = integerIn(pick(sensorOptions, "noiseSeed", "sensor.noiseSeed", DEFAULTS.noiseSeed, "Fixed noise seed 1 (deterministic output)."), "sensor.noiseSeed", 0, 0xffffffff);
|
|
826
959
|
const antiAliasing = sensorOptions.antiAliasing === undefined ? undefined : parseAntiAliasing(sensorOptions.antiAliasing, pitch);
|
|
960
|
+
const antiAliasingFast = antiAliasing ? fastOverride(sensorOptions.antiAliasing, "sensor.antiAliasing") : undefined;
|
|
827
961
|
// Pixel aperture (2.3): N × N midpoint nodes over the square photosite; 1 = the default centre sample.
|
|
828
|
-
let boxSamples = 1;
|
|
962
|
+
let boxSamples = 1, boxFast;
|
|
829
963
|
if (sensorOptions.pixelAperture !== undefined && sensorOptions.pixelAperture !== "point") {
|
|
830
964
|
const a = sensorOptions.pixelAperture;
|
|
831
965
|
if (a === "box")
|
|
832
966
|
boxSamples = SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES;
|
|
833
967
|
else {
|
|
834
|
-
const record = allowKeys(a, "sensor.pixelAperture", ["kind", "samplesPerAxis"]);
|
|
968
|
+
const record = allowKeys(a, "sensor.pixelAperture", ["kind", "samplesPerAxis", "fast"]);
|
|
835
969
|
if (record.kind !== "box")
|
|
836
|
-
fail("sensor.pixelAperture must be \"point\", \"box\" or { kind: \"box\", samplesPerAxis }.");
|
|
970
|
+
fail("sensor.pixelAperture must be \"point\", \"box\" or { kind: \"box\", samplesPerAxis, fast }.");
|
|
971
|
+
boxFast = fastOverride(record, "sensor.pixelAperture");
|
|
837
972
|
boxSamples = record.samplesPerAxis === undefined ? SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES : integerIn(record.samplesPerAxis, "sensor.pixelAperture.samplesPerAxis", 1, 16);
|
|
838
973
|
}
|
|
839
974
|
}
|
|
@@ -843,7 +978,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
843
978
|
if (displayView) {
|
|
844
979
|
const unsupported = (option, reason) => fail("simulateDisplayView does not support " + option + ": " + reason);
|
|
845
980
|
if (pupil)
|
|
846
|
-
unsupported("lens.pupilSampling", "
|
|
981
|
+
unsupported("lens.pupilSampling", "pupil rays move each query off the site lattice the expected-signal tables use; lens.defocus renders the defocus of the scene plane on the lattice.");
|
|
847
982
|
if (lensFieldMapping)
|
|
848
983
|
unsupported("lens.projection or lens.distortion", "the expected signal assumes the ideal rectilinear, distortion-free mapping of the site lattice.");
|
|
849
984
|
if (vignetting)
|
|
@@ -865,10 +1000,35 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
865
1000
|
layout: { kind: "periodic-mosaic", repeatWidthSites: 2, repeatHeightSites: 2, siteChannelIds: ["red", "green", "green", "blue"], anchor: "native-sensor-top-left-site" } };
|
|
866
1001
|
// Demosaic (2.0): built-in Bayer kernels and border handling. Omitted, the 1.0 bilinear kernels and a
|
|
867
1002
|
// one-site rendered border are used unchanged.
|
|
868
|
-
|
|
1003
|
+
// #395: the record form { kind, fast } names the same demosaic.
|
|
1004
|
+
let demosaicOption, demosaicFast;
|
|
1005
|
+
if (outputOptions.demosaic !== undefined && typeof outputOptions.demosaic === "object" && outputOptions.demosaic !== null) {
|
|
1006
|
+
const record = allowKeys(outputOptions.demosaic, "output.demosaic", ["kind", "fast"]);
|
|
1007
|
+
if (record.kind !== "bilinear" && record.kind !== "ahd")
|
|
1008
|
+
fail("output.demosaic must be bilinear or ahd, or { kind, fast }.");
|
|
1009
|
+
demosaicOption = record.kind;
|
|
1010
|
+
demosaicFast = fastOverride(record, "output.demosaic");
|
|
1011
|
+
}
|
|
1012
|
+
else
|
|
1013
|
+
demosaicOption = outputOptions.demosaic;
|
|
1014
|
+
const borderOption = outputOptions.borderHandling;
|
|
869
1015
|
if (demosaicOption !== undefined && demosaicOption !== "bilinear" && demosaicOption !== "ahd")
|
|
870
1016
|
fail("output.demosaic must be bilinear or ahd.");
|
|
871
1017
|
const ahd = demosaicOption === "ahd";
|
|
1018
|
+
// #395: exact or fast per option. No option has a fast method yet, so each runs exact and reports false.
|
|
1019
|
+
if (top.fast !== undefined && typeof top.fast !== "boolean")
|
|
1020
|
+
fail("fast must be true or false.");
|
|
1021
|
+
const fastTop = top.fast === true;
|
|
1022
|
+
const fastInUse = [["lens.diffraction", diffractionRadiusOption !== undefined, diffractionFast],
|
|
1023
|
+
["sensor.antiAliasing", antiAliasing !== undefined, antiAliasingFast], ["sensor.pixelAperture.box", boxAperture, boxFast], ["output.demosaic.ahd", ahd, demosaicFast]];
|
|
1024
|
+
const fastApplied = {};
|
|
1025
|
+
for (const [option, used] of fastInUse)
|
|
1026
|
+
if (used)
|
|
1027
|
+
fastApplied[option] = false;
|
|
1028
|
+
const fastReport = Object.freeze({
|
|
1029
|
+
requested: fastTop || [diffractionFast, antiAliasingFast, boxFast, demosaicFast].some((value) => value === true),
|
|
1030
|
+
applied: Object.freeze(fastApplied), assurance: "engine-reference"
|
|
1031
|
+
});
|
|
872
1032
|
if (borderOption !== undefined && borderOption !== "margin" && borderOption !== "mirror")
|
|
873
1033
|
fail("output.borderHandling must be margin or mirror.");
|
|
874
1034
|
const builtInDemosaic = demosaicOption === undefined ? undefined : createBuiltInBayerDemosaicProfiles({ kernel: "bilinear",
|
|
@@ -969,10 +1129,14 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
969
1129
|
const illuminant = resolveCaptureIlluminant(illuminantOption);
|
|
970
1130
|
const illuminanceLux = positive(pick(top, "illuminanceLux", "illuminanceLux", SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX, "π·10·N²/(0.65·t·S·0.18) lux at f/8, 1/125 s, ISO 100 (CIPA DC-004 standard output sensitivity H = 10/S and camera equation H = 0.65·L·t/N²): an 18 % grey card then gets the reference exposure for the default settings. It does not follow the caller's exposure settings."), "illuminanceLux");
|
|
971
1131
|
const irradianceScale = calculateIlluminantIrradianceScale(illuminant.shape, illuminanceLux);
|
|
972
|
-
let chart, callerScene;
|
|
1132
|
+
let chart, callerScene, materialLayers;
|
|
973
1133
|
const sceneRecord = sceneOption;
|
|
974
1134
|
if ("kind" in sceneRecord) {
|
|
975
|
-
callerScene
|
|
1135
|
+
// #491: a declared material-layers scene is its callerScene (the same validation, materials, digest and bytes as passing that scene);
|
|
1136
|
+
// the resolved layers are kept for the display view's GPU renderer.
|
|
1137
|
+
if (sceneRecord.kind === "material-layers")
|
|
1138
|
+
materialLayers = resolveChartMaterialLayers({ scene: sceneOption, aspect: chartSize ? chartSize.heightM / chartSize.widthM : aspect });
|
|
1139
|
+
callerScene = requireCallerScene(materialLayers ? materialLayers.callerScene : sceneOption);
|
|
976
1140
|
}
|
|
977
1141
|
else {
|
|
978
1142
|
chart = resolveChartScene({ scene: sceneOption, aspect: chartSize ? chartSize.heightM / chartSize.widthM : aspect });
|
|
@@ -1095,6 +1259,19 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1095
1259
|
fNumber = resolution.resolvedSettings.aperture ?? fNumber;
|
|
1096
1260
|
shutterSeconds = resolution.resolvedSettings.shutterSeconds ?? shutterSeconds;
|
|
1097
1261
|
const resolvedIso = resolution.resolvedSettings.iso ?? iso;
|
|
1262
|
+
const defocusDisc = defocusModel === undefined ? undefined :
|
|
1263
|
+
resolveDefocusOption({ run, assume, focalLengthMm, fNumber, focusDistanceM, subjectDistanceM, pitch, diffraction: diffractionRadiusOption !== undefined });
|
|
1264
|
+
// #492: the capture applies the disc as an object-space convolution, which needs every disc point a site sees to be another site's scene point.
|
|
1265
|
+
if (defocusDisc) {
|
|
1266
|
+
const reason = rotating ? "camera rotation (motion.angularVelocityRadPerSec)" : lensFieldMapping ? "lens.projection or lens.distortion" : vignetting ? "lens.vignetting" : undefined;
|
|
1267
|
+
if (reason !== undefined)
|
|
1268
|
+
fail("simulateCapture does not support lens.defocus out of focus with " + reason + ": the defocus disc is applied as an object-space convolution " +
|
|
1269
|
+
"on the site lattice, and " + reason + " moves the scene points the sites see off that lattice. Set lens.focusDistanceM equal to subjectDistanceM, or omit one of the two options.");
|
|
1270
|
+
}
|
|
1271
|
+
// #492: the disc on the site lattice (radius 0 for a disc inside one cell: the identity, still applied).
|
|
1272
|
+
const defocusKernel = defocusDisc && !displayView ? createDefocusDiscSiteKernel(defocusDisc.diameterSites) ?? undefined : undefined;
|
|
1273
|
+
// The display view's disc (#492, step 2b): its expected-signal tables apply it (simulate-display-view.ts).
|
|
1274
|
+
const displayViewDefocus = displayView ? defocusDisc : undefined;
|
|
1098
1275
|
// Sensor readout for the ISO setting. Standard output sensitivity S = 10/H (CIPA DC-004 §3.2) is defined at the
|
|
1099
1276
|
// output, independent of pixel size, while the collected charge is proportional to the sensitive-aperture area
|
|
1100
1277
|
// (EMVA 1288 Release 4.0, Eq. 3). The tier's signal chain is anchored to the 6 µm aperture; at another area the
|
|
@@ -1310,7 +1487,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1310
1487
|
uncertainty: { kind: "not-quantified", limitation: "Analytic planar scene." },
|
|
1311
1488
|
fidelity: { spectral: "wavelength-resolved", material: "spectral-data", visibility: "resolved", directTransport: "approximation", indirectTransport: "not-modeled" },
|
|
1312
1489
|
wavelengthChangingTransportModeled: false, volumetricTransportModeled: false, polarizationModeled: false, evidence, limitations: ["Engine-authored planar chart or caller scene function.",
|
|
1313
|
-
...(diffractionObjectSpace ? ["Radiance is the scene radiance convolved with the lens diffraction PSF mapped onto the scene plane (object-space convolution, request.diffraction)."] : [])
|
|
1490
|
+
...(diffractionObjectSpace ? ["Radiance is the scene radiance convolved with the lens diffraction PSF mapped onto the scene plane (object-space convolution, request.diffraction)."] : []),
|
|
1491
|
+
...(defocusKernel ? ["Radiance is the scene radiance convolved with the geometric defocus disc mapped onto the scene plane (object-space convolution, lens.defocus)."] : [])] }
|
|
1314
1492
|
};
|
|
1315
1493
|
const lensId = lensExposure.profileId;
|
|
1316
1494
|
const opticsProfile = { schemaVersion: "0.1.0", profileId: lensId + ":facade-transmission", profileVersion: SIMULATE_CAPTURE_FACADE_VERSION, lensProfileId: lensId,
|
|
@@ -1333,9 +1511,11 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1333
1511
|
maximumSubintervalWidthNanometers: spectralStep }]));
|
|
1334
1512
|
// Diffraction (2.1): Airy kernels at the resolved f-number on the site pitch, one per spectral node of the sensor quadrature.
|
|
1335
1513
|
let diffraction, diffractionPsf;
|
|
1514
|
+
// The spectral nodes of the sensor quadrature, over all channels, ascending.
|
|
1515
|
+
const quadratureWavelengths = () => [...new Set(CHANNELS.flatMap((channelId) => calculateSensorSpectralQuadrature({ ...spectralSamplingByChannel[channelId], colorSamplingProfile,
|
|
1516
|
+
spectralResponseProfile: spectralResponseProfiles[channelId] }).value.nodes.map((node) => node.wavelengthNanometers)))].sort((a, b) => a - b);
|
|
1336
1517
|
if (diffractionRadiusOption !== undefined) {
|
|
1337
|
-
const wavelengths =
|
|
1338
|
-
spectralResponseProfile: spectralResponseProfiles[channelId] }).value.nodes.map((node) => node.wavelengthNanometers)))].sort((a, b) => a - b);
|
|
1518
|
+
const wavelengths = quadratureWavelengths();
|
|
1339
1519
|
const lambdaN = wavelengths[wavelengths.length - 1] / 1000 * fNumber * imageDistanceMm / focalLengthMm;
|
|
1340
1520
|
// The environment resolves the PSF at each pre-filter source point, up to the largest split offset outside the imaging area.
|
|
1341
1521
|
const aaReachMm = antiAliasing ? Math.max(...antiAliasing.components.map((c) => Math.max(Math.abs(c.offsetMicrometers.x), Math.abs(c.offsetMicrometers.y)))) / 1000 : 0;
|
|
@@ -1343,13 +1523,18 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1343
1523
|
// larger tail energy outside it is listed per wavelength like any other (2.2.1 states the cap in the explanation).
|
|
1344
1524
|
const thirdRingSites = Math.max(1, Math.ceil(AIRY_THIRD_ZERO_PER_LAMBDA_N * lambdaN / pitch));
|
|
1345
1525
|
const kernelRadiusSamples = diffractionRadiusOption ?? Math.min(32, thirdRingSites);
|
|
1346
|
-
|
|
1526
|
+
const profileInput = { aperture: "circular", focalLengthMm, apertureFNumber: fNumber, focus: { kind: "finite", distanceM: focusDistanceM },
|
|
1347
1527
|
wavelengthsNm: wavelengths, samplePitchMicrometers: pitch, kernelRadiusSamples, fieldHalfExtentMm: aaReachMm === 0 ? { x: imagingArea.widthMm / 2, y: imagingArea.heightMm / 2 } :
|
|
1348
1528
|
{ x: imagingArea.widthMm / 2 + aaReachMm, y: imagingArea.heightMm / 2 + aaReachMm },
|
|
1349
|
-
profileId: "facade-diffraction-airy" }
|
|
1529
|
+
profileId: "facade-diffraction-airy" };
|
|
1530
|
+
// #449: a copy of the profile kept for this input; it equals createDiffractionLimitedPsfProfile(profileInput).value field by
|
|
1531
|
+
// field, so the registration below (keyed by profileInput) holds for it.
|
|
1532
|
+
diffraction = cachedDiffractionProfile(profileInput);
|
|
1350
1533
|
diffractionPsf = { kind: "sampled-local", configuration: { psf: { profile: diffraction.profile, focalLengthMm, focus: { kind: "finite", distanceM: focusDistanceM },
|
|
1351
1534
|
apertureFNumber: fNumber, signedDefocusImagePlaneMicrometers: 0 }, psfWavelengthBasis: { value: "vacuum", evidence },
|
|
1352
1535
|
spatialModel: { kind: "destination-local-shift-invariant-approximation", evidence, limitation: "Diffraction-limited Airy PSF, the same at every field point." } } };
|
|
1536
|
+
// Requests with the same profile input and PSF fields share their site diffraction kernels (optics/site-diffraction-kernel.ts).
|
|
1537
|
+
registerDiffractionPsfKey(diffractionPsf.configuration.psf, profileInput);
|
|
1353
1538
|
assume("lens.diffraction", "model", { model: diffraction.model, aperture: "circular-aperture", fNumber, workingFNumber: diffraction.workingFNumber,
|
|
1354
1539
|
kernelRadiusSamples, samplePitchMicrometers: pitch, maximumTruncatedEnergyFraction: diffraction.maximumTruncatedEnergyFraction,
|
|
1355
1540
|
truncatedEnergyByWavelength: diffraction.wavelengths.map((w) => ({ wavelengthNm: w.wavelengthNm, truncatedEnergyFraction: w.truncatedEnergyFraction })),
|
|
@@ -1361,9 +1546,36 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1361
1546
|
"Applied through the environment PSF quadrature (one scene query per kernel tap), because camera rotation, a lens field mapping or vignetting moves the taps off the site lattice. ") +
|
|
1362
1547
|
"No defocus, aberration or pupil clipping; " + (boxAperture ? "the pixel aperture is integrated as declared by sensor.pixelAperture." : "the sensor pixel aperture is still point-sampled at the site centre."));
|
|
1363
1548
|
}
|
|
1549
|
+
// #492: each spectral node's kernel for lens.defocus out of focus: the disc, or with lens.diffraction (always on the object-space path here)
|
|
1550
|
+
// the node's diffraction kernel convolved with the disc, radius R_diffraction + R_defocus (built on first use, kept for the request).
|
|
1551
|
+
let defocusSiteKernels;
|
|
1552
|
+
if (defocusKernel) {
|
|
1553
|
+
if (diffraction) {
|
|
1554
|
+
const diffractionRadius = diffraction.kernelRadiusSamples, combined = new Map(), profileWavelengths = diffraction.profile.axes.wavelengthNm;
|
|
1555
|
+
const radius = diffractionRadius + defocusKernel.radius;
|
|
1556
|
+
// At most 32 + 64 = 96 sites (the two caps). Neither the expected-signal table nor the provider's own gather (diffraction-provider.ts)
|
|
1557
|
+
// uses the public 32-site limit of calculateIrradianceConvolution; the combination costs at most about 0.1 s per spectral node
|
|
1558
|
+
// (measured, docs/QUICK_START.md "Optics"), so no tighter limit applies.
|
|
1559
|
+
const psfConfiguration = diffractionPsf.configuration.psf;
|
|
1560
|
+
defocusSiteKernels = { radius, wavelengthsNm: profileWavelengths, at(wavelengthNm) {
|
|
1561
|
+
let kernel = combined.get(wavelengthNm);
|
|
1562
|
+
if (!kernel) {
|
|
1563
|
+
if (!profileWavelengths.includes(wavelengthNm))
|
|
1564
|
+
fail("simulateCapture: a spectral node has no diffraction kernel.");
|
|
1565
|
+
kernel = convolveSiteKernels(siteDiffractionKernel(psfConfiguration, wavelengthNm, pitch, diffractionRadius, "simulateCapture: diffraction kernels must sample the site pitch exactly."), diffractionRadius, defocusKernel.weights, defocusKernel.radius);
|
|
1566
|
+
combined.set(wavelengthNm, kernel);
|
|
1567
|
+
}
|
|
1568
|
+
return kernel;
|
|
1569
|
+
} };
|
|
1570
|
+
}
|
|
1571
|
+
else
|
|
1572
|
+
defocusSiteKernels = { radius: defocusKernel.radius, wavelengthsNm: quadratureWavelengths(), at: () => defocusKernel.weights };
|
|
1573
|
+
}
|
|
1364
1574
|
const psf = lensPsf ?? (diffractionObjectSpace ? { kind: "not-applied", evidence,
|
|
1365
1575
|
limitation: "No environment PSF: lens diffraction is applied upstream by the scene provider as an object-space convolution (request.diffraction)." } : undefined) ??
|
|
1366
|
-
diffractionPsf ?? { kind: "not-applied", evidence,
|
|
1576
|
+
diffractionPsf ?? (defocusSiteKernels ? { kind: "not-applied", evidence,
|
|
1577
|
+
limitation: "No environment PSF: lens defocus is applied upstream by the scene provider as an object-space convolution (lens.defocus)." } : undefined) ??
|
|
1578
|
+
{ kind: "not-applied", evidence, limitation: "No lens PSF (point optics)." };
|
|
1367
1579
|
const pupilRecord = pupil ? { kind: "ideal-uniform-circular-pupil", radialSampleCount: pupil.radialSamples, angularSampleCount: pupil.angularSamples, evidence,
|
|
1368
1580
|
limitation: "Ideal uniform circular pupil; finite ray sampling, no convergence claim." } : undefined;
|
|
1369
1581
|
const chargeSamplingProfile = { schemaVersion: "0.1.0", profileId: IDS.chargeSampling, completenessProfileId: IDS.completeness, scientificStatus: "approximation",
|
|
@@ -1400,14 +1612,19 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1400
1612
|
const readTile = async (request) => ({ ...request, sites: Array.from({ length: request.width }, (_, j) => siteInput(request.y * nativeWidth + request.x + j)) });
|
|
1401
1613
|
const yieldControl = createHostYield().yieldControl;
|
|
1402
1614
|
const perSite = queriesPerSite * (diffraction && psf === diffractionPsf ? (2 * diffraction.kernelRadiusSamples + 1) ** 2 : 1);
|
|
1403
|
-
let diffractionMs = 0, demosaicMs, encodeMs;
|
|
1404
|
-
const diffractedProvider = (window) =>
|
|
1405
|
-
wavelengthsNm:
|
|
1406
|
-
sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit, onConvolution: (ms) => {
|
|
1407
|
-
// Region (2.6): convolve only the lattice the region's native sites query; the values are the whole render's.
|
|
1615
|
+
let diffractionMs = 0, defocusMs = 0, demosaicMs, encodeMs;
|
|
1616
|
+
const diffractedProvider = (window) => defocusSiteKernels ? createDiffractedPlanarProvider({ base: provider, siteKernels: defocusSiteKernels.at,
|
|
1617
|
+
wavelengthsNm: defocusSiteKernels.wavelengthsNm, kernelRadius: defocusSiteKernels.radius, nativeWidth, nativeHeight, outputWidth: width,
|
|
1618
|
+
sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit, onConvolution: (ms) => { defocusMs += ms; },
|
|
1408
1619
|
...(window ? { window } : {}),
|
|
1409
|
-
|
|
1410
|
-
|
|
1620
|
+
...(antiAliasing || boxSamples > 1 ? { queryOffsetsSites: apertureNodeOffsets(boxSamples).flatMap((node) => (antiAliasing ? antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 } }]).map((c) => ({ x: node.x - c.offsetMicrometers.x / pitch, y: node.y - c.offsetMicrometers.y / pitch }))) } : {}) }) :
|
|
1621
|
+
diffraction && diffractionObjectSpace ? createDiffractedPlanarProvider({ base: provider, psf: diffractionPsf.configuration.psf,
|
|
1622
|
+
wavelengthsNm: diffraction.profile.axes.wavelengthNm, kernelRadius: diffraction.kernelRadiusSamples, nativeWidth, nativeHeight, outputWidth: width,
|
|
1623
|
+
sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit, onConvolution: (ms) => { diffractionMs += ms; },
|
|
1624
|
+
// Region (2.6): convolve only the lattice the region's native sites query; the values are the whole render's.
|
|
1625
|
+
...(window ? { window } : {}),
|
|
1626
|
+
// The environment queries each site at each aperture node minus each split offset (sites, +x right, +y down).
|
|
1627
|
+
...(antiAliasing || boxSamples > 1 ? { queryOffsetsSites: apertureNodeOffsets(boxSamples).flatMap((node) => (antiAliasing ? antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 } }]).map((c) => ({ x: node.x - c.offsetMicrometers.x / pitch, y: node.y - c.offsetMicrometers.y / pitch }))) } : {}) }) : provider;
|
|
1411
1628
|
// Built on first use: a capture that takes the tabulated path (#394 step 2) never queries it, and its kernels cost a setup.
|
|
1412
1629
|
let rawProviderBuilt;
|
|
1413
1630
|
const rawProvider = () => (rawProviderBuilt ??= diffractedProvider(regionNative));
|
|
@@ -1441,9 +1658,11 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1441
1658
|
delete environment.evaluateApertureRadiance;
|
|
1442
1659
|
environments[channel] = environment;
|
|
1443
1660
|
}
|
|
1444
|
-
// The responses are a function of these records
|
|
1445
|
-
//
|
|
1446
|
-
|
|
1661
|
+
// The responses are a function of these records without their scene bindings: the radiance is replaced at every node,
|
|
1662
|
+
// and the bindings (provider, illumination and material profiles) are only checked, never enter a number. So a chart,
|
|
1663
|
+
// illuminant or illuminance change keeps the entry (#443). A request split into region tiles builds the same records for
|
|
1664
|
+
// every tile. The entries are kept across calls (the last CHANNEL_RESPONSE_CACHE_SIZE).
|
|
1665
|
+
const key = JSON.stringify(CHANNELS.map((c) => ({ ...environments[c], sceneBindings: undefined })));
|
|
1447
1666
|
const copy = (responses) => Object.fromEntries(CHANNELS.map((c) => [c, new Map(responses[c])]));
|
|
1448
1667
|
const cached = channelResponseCache.get(key);
|
|
1449
1668
|
let electronsPerRadiance;
|
|
@@ -1454,15 +1673,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1454
1673
|
}
|
|
1455
1674
|
else {
|
|
1456
1675
|
electronsPerRadiance = {};
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
.photo.value.photoSignal.expectedGeneratedElectronCount;
|
|
1462
|
-
const nodes = new Set();
|
|
1463
|
-
electrons((nm) => { nodes.add(nm); return 1; });
|
|
1464
|
-
electronsPerRadiance[channel] = new Map([...nodes].sort((a, b) => a - b).map((node) => [node, electrons((nm) => (nm === node ? 1 : 0))]));
|
|
1465
|
-
}
|
|
1676
|
+
// One chain execution per channel, each node's electrons read from its own rate term (#443; exact, see channel-response.ts).
|
|
1677
|
+
// The records are built above for this call only, so they are planned as engine-owned data.
|
|
1678
|
+
for (const channel of CHANNELS)
|
|
1679
|
+
electronsPerRadiance[channel] = calculateChannelElectronsPerRadiance(environments[channel], (request) => provider.evaluateRadiance(request), caller);
|
|
1466
1680
|
// Callers get their own maps; the cached ones are never handed out.
|
|
1467
1681
|
channelResponseCache.set(key, copy(electronsPerRadiance));
|
|
1468
1682
|
if (channelResponseCache.size > CHANNEL_RESPONSE_CACHE_SIZE)
|
|
@@ -1485,7 +1699,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1485
1699
|
// Display view: the renderer that simulate-display-view.ts passes in reads this request's resolved state, so a bundle
|
|
1486
1700
|
// that only captures does not contain the display view.
|
|
1487
1701
|
if (displayView)
|
|
1488
|
-
return await renderView({ ahd, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, chartReport, chartSize, diffraction, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl, kelvinWhiteBalanceInput });
|
|
1702
|
+
return await renderView({ ahd, fastReport, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, materialLayers, chartReport, chartSize, diffraction, defocus: displayViewDefocus, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl, kelvinWhiteBalanceInput });
|
|
1489
1703
|
// Ideal reference (2.3): the scene without camera effects, at the capture's own exposure, white balance and encoding.
|
|
1490
1704
|
if (idealReference) {
|
|
1491
1705
|
const referenceStarted = clockMs();
|
|
@@ -1610,12 +1824,20 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1610
1824
|
const { electronsPerRadiance, wavelengths } = axisChannelResponse("simulateCapture");
|
|
1611
1825
|
const perNode = CHANNELS.map((c) => Float64Array.from(wavelengths, (nm) => electronsPerRadiance[c].get(nm) ?? 0));
|
|
1612
1826
|
const sceneMaterials = chart ? chart.materials : callerScene.materials, materialRadiance = provider.materialRadiance;
|
|
1613
|
-
const { signalInput } = buildExpectedSignalInput({ nativeWidth, nativeHeight, width, wavelengths, perNode, apertureNodes: apertureNodeOffsets(boxSamples), boxSamples, antiAliasing, pitch,
|
|
1614
|
-
diffraction: diffraction ? { radius: diffraction.kernelRadiusSamples, psf: diffractionPsf.configuration.psf, profileWavelengths: diffraction.profile.axes.wavelengthNm } : undefined,
|
|
1827
|
+
const { signalInput: builtSignalInput } = buildExpectedSignalInput({ nativeWidth, nativeHeight, width, wavelengths, perNode, apertureNodes: apertureNodeOffsets(boxSamples), boxSamples, antiAliasing, pitch,
|
|
1828
|
+
diffraction: diffraction && !defocusSiteKernels ? { radius: diffraction.kernelRadiusSamples, psf: diffractionPsf.configuration.psf, profileWavelengths: diffraction.profile.axes.wavelengthNm } : undefined,
|
|
1615
1829
|
materials: { materialAt: provider.chartMaterialAt, whiteRadiance: (nm) => irradianceScale * illuminant.shape.evaluate(nm) / Math.PI,
|
|
1616
1830
|
list: [...sceneMaterials.map((m, index) => (m.kind === "neutral" ? { neutral: m.reflectance } : { radiance: (nm) => materialRadiance(index, nm) })),
|
|
1617
1831
|
...(chartSize ? [{ neutral: chartSize.surroundReflectance }] : [])] },
|
|
1618
1832
|
radianceAt: provider.radianceAt, imagingArea, focalLengthMm, focusDistanceM, subjectDistanceM, metresPerFrameUnit, caller: "simulateCapture" });
|
|
1833
|
+
// #492: with lens.defocus the table convolves the lattice radiance with each node's defocus (or combined) kernel, on the
|
|
1834
|
+
// lattice formula, exactly as the object-space provider does.
|
|
1835
|
+
let signalInput = builtSignalInput;
|
|
1836
|
+
if (defocusSiteKernels) {
|
|
1837
|
+
const lattice = { ...builtSignalInput };
|
|
1838
|
+
delete lattice.queryPoint;
|
|
1839
|
+
signalInput = { ...lattice, kernels: { radius: defocusSiteKernels.radius, perNode: wavelengths.map((nm) => defocusSiteKernels.at(nm)) } };
|
|
1840
|
+
}
|
|
1619
1841
|
// The verification reads only the region's top-left 2 × 2 block, so its diffraction convolves only that block's lattice; the
|
|
1620
1842
|
// window never changes a value (see diffraction-provider.ts), so the chain it runs is the whole render's.
|
|
1621
1843
|
const verificationProvider = diffractedProvider({ x: regionNative?.x ?? 0, y: regionNative?.y ?? 0, width: 2, height: 2 });
|
|
@@ -1827,7 +2049,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1827
2049
|
}
|
|
1828
2050
|
const ranOptions = new Set([...(pupil ? ["lens.pupilSampling"] : []), ...(projection !== undefined ? ["lens.projection"] : []),
|
|
1829
2051
|
...(distortion ? ["lens.distortion"] : []), ...(vignetting ? ["lens.vignetting"] : []), ...(lensPsf ? ["lens.psf"] : []),
|
|
1830
|
-
...(diffraction ? ["lens.diffraction"] : []), ...(antiAliasing ? ["sensor.antiAliasing"] : []), ...(boxAperture ? ["sensor.pixelAperture.box"] : []),
|
|
2052
|
+
...(diffraction ? ["lens.diffraction"] : []), ...(defocusSiteKernels ? ["lens.defocus"] : []), ...(antiAliasing ? ["sensor.antiAliasing"] : []), ...(boxAperture ? ["sensor.pixelAperture.box"] : []),
|
|
1831
2053
|
...(temporalSampleCount > 1 ? ["sensor.temporalSampleCount"] : []), ...(rotating ? ["motion.rotation"] : []),
|
|
1832
2054
|
...(rollingSchedule ? ["motion.shutter.rolling"] : []), ...(mirrorBorder ? ["output.borderHandling.mirror"] : []), ...(region ? ["output.region"] : []),
|
|
1833
2055
|
...(needsAuto ? ["output.whiteBalance.auto"] : []), ...(ahd ? ["output.demosaic.ahd"] : []),
|
|
@@ -1847,15 +2069,16 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1847
2069
|
framedWidthM: 2 * metresPerFrameUnit, framedHeightM: 2 * metresPerFrameUnit * aspect, ...(chartReport ? { chart: chartReport } : {}), sourceSha256 },
|
|
1848
2070
|
diagnostics: { providerEvaluationCount, highlight: developed.highlight, rendering: developed.diagnostics, ...(gamutMappingDiagnostics ? { gamutMapping: gamutMappingDiagnostics } : {}),
|
|
1849
2071
|
timingsMs: { raw: rawMs, whiteBalance: wbMs, development: devMs, jpeg: jpegMs, total: Date.now() - started,
|
|
1850
|
-
...(diffraction && diffractionObjectSpace ? { diffraction: diffractionMs } : {}), ...(demosaicMs === undefined ? {} : { demosaic: demosaicMs }),
|
|
2072
|
+
...(diffraction && diffractionObjectSpace && !defocusSiteKernels ? { diffraction: diffractionMs } : {}), ...(defocusSiteKernels ? { defocus: defocusMs } : {}), ...(demosaicMs === undefined ? {} : { demosaic: demosaicMs }),
|
|
1851
2073
|
...(encodeMs === undefined ? {} : { encode: encodeMs }) },
|
|
1852
|
-
options: Object.freeze(SIMULATE_CAPTURE_COST_OPTIONS.filter((option) => ranOptions.has(option))) },
|
|
2074
|
+
options: Object.freeze(SIMULATE_CAPTURE_COST_OPTIONS.filter((option) => ranOptions.has(option))), fast: fastReport },
|
|
1853
2075
|
provenance: { scientificStatus: "approximation", cameraPresetId: preset.presetId, cameraPresetVersion: preset.presetVersion,
|
|
1854
2076
|
evidence: [...FACADE_EVIDENCE, ...preset.evidence],
|
|
1855
2077
|
composedContracts: ["resolveGenericEquipmentExposureCapabilities", "meterRelativeExposure", "createExposureMeterTargetFromMeteringResult",
|
|
1856
2078
|
"resolve" + (mode === "manual" ? "Manual" : mode === "aperture-priority" ? "AperturePriority" : mode === "shutter-priority" ? "ShutterPriority" : "ProgramAuto") + "ExposureMode",
|
|
1857
2079
|
...(rollingSchedule ? ["resolveElectronicRollingShutterExposureSchedule", "createRollingShutterCaptureExposureSchedule"] : []),
|
|
1858
|
-
...(diffraction ? ["createDiffractionLimitedPsfProfile", ...(diffractionObjectSpace ? ["resolveLensPsfConvolutionKernels", "calculateIrradianceConvolution"] : [])] : []),
|
|
2080
|
+
...(diffraction ? ["createDiffractionLimitedPsfProfile", ...(diffractionObjectSpace ? ["resolveLensPsfConvolutionKernels", ...(defocusSiteKernels ? [] : ["calculateIrradianceConvolution"])] : [])] : []),
|
|
2081
|
+
...(defocusSiteKernels ? ["calculateDefocusCircle", "defocus disc site kernel (object-space convolution)"] : []),
|
|
1859
2082
|
...(antiAliasing ? ["resolveAntiAliasingSpatialKernel"] : []), "resolveGenericIsoSignalChain", "createNativeEnvironmentRawTask", ...(needsAuto ? ["estimateAutoWhiteBalance"] : []),
|
|
1860
2083
|
...(builtInDemosaic ? ["createBuiltInBayerDemosaicProfiles"] : []), ...(ahd ? ["createAhdDemosaicTask"] : []),
|
|
1861
2084
|
...(ahd ? ["native RAW development operator (colour, after AHD)", ...(linearPath ? ["resolveRgbColorSpace", ...(perceptual ? ["prepareRgbGamutMapping"] : [])] : ["SDR rendering kernel"])] : linearPath ? ["native RAW development operator (linear, pre-clip)", "resolveRgbColorSpace", ...(perceptual ? ["prepareRgbGamutMapping"] : [])] : ["createNativeRawDevelopmentTask"]),
|