@photivra/engine 2.10.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 +139 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +21 -3
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/core/portable-math.d.ts +6 -0
- package/dist/core/portable-math.js +39 -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/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 +71 -2
- 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 +48 -1
- package/dist/facade/display-view.js +7 -7
- 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 +16 -0
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +48 -3
- package/dist/facade/expected-signal.js +8 -0
- 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 +32 -5
- package/dist/facade/simulate-capture.js +148 -24
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +31 -3
- package/dist/facade/simulate-display-view.js +72 -12
- 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 +55 -3
- 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 +4 -1
- package/dist/index.js +2 -0
- 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/docs/API_REFERENCE.md +45 -1
- package/docs/QUICK_START.md +102 -3
- package/docs/USAGE.md +52 -2
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +117 -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-scene-radiance.md +3 -2
- package/docs/api/facade-simulate-capture.md +23 -6
- package/docs/api/facade-simulate-display-view.md +17 -2
- package/docs/api/gpu-image-kernel-accelerator.md +10 -3
- package/docs/api/gpu-image-kernel-execution.md +71 -4
- package/docs/api/gpu-webgpu-switch.md +23 -0
- package/package.json +3 -1
|
@@ -72,7 +72,8 @@ 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 } from "../optics/site-diffraction-kernel.js";
|
|
75
|
+
import { registerDiffractionPsfKey, siteDiffractionKernel } from "../optics/site-diffraction-kernel.js";
|
|
76
|
+
import { convolveSiteKernels, createDefocusDiscSiteKernel, requireDefocusDiscKernelRadius, resolveDefocusDisc } from "../optics/defocus-disc-kernel.js";
|
|
76
77
|
import { createDiffractedPlanarProvider } from "./diffraction-provider.js";
|
|
77
78
|
import { resolveGenericPrimeLens } from "../equipment/generic-prime-lenses.js";
|
|
78
79
|
import { calculateChannelElectronsPerRadiance } from "./channel-response.js";
|
|
@@ -84,6 +85,7 @@ import { prepareRgbGamutMapping } from "../color/gamut-mapping.js";
|
|
|
84
85
|
import { serializeNativeRawDng } from "../capture/native-raw-dng.js";
|
|
85
86
|
import { parseSensorReadoutConversionProfile, resolveSensorReadoutRegime } from "../sensor/raw-readout.js";
|
|
86
87
|
import { resolveChartScene, chartMaterialInputs, CHART_SCENE_PRESETS } from "./chart-scenes.js";
|
|
88
|
+
import { resolveChartMaterialLayers } from "./chart-material-layers.js";
|
|
87
89
|
import { resolveCaptureIlluminant, calculateIlluminantIrradianceScale, buildPlanarSceneRadianceProvider, requireCallerScene, FACADE_SPECTRAL_RANGE_NANOMETERS, resolveChartSizeOption, PHOTOPIC_MAXIMUM_LUMINOUS_EFFICACY_LM_PER_W, planarProviderMaterialOfDirection } from "./scene-radiance.js";
|
|
88
90
|
/** Version of how the facade builds requests. It changes whenever the same input would build a different request. */
|
|
89
91
|
export const SIMULATE_CAPTURE_FACADE_VERSION = "1.0.0";
|
|
@@ -94,7 +96,7 @@ export const SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES = 4;
|
|
|
94
96
|
* a capture does. `motion.rotation` is a non-zero angular velocity; `sensor.temporalSampleCount` a count above 1.
|
|
95
97
|
*/
|
|
96
98
|
export const SIMULATE_CAPTURE_COST_OPTIONS = Object.freeze(["lens.pupilSampling", "lens.projection", "lens.distortion", "lens.vignetting", "lens.psf",
|
|
97
|
-
"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",
|
|
98
100
|
"output.whiteBalance.auto", "output.demosaic.ahd", "output.highlightMode.reconstruct", "output.colorEncoding.wide-gamut", "output.gamutMapping.perceptual",
|
|
99
101
|
"output.format.jpeg", "output.format.png", "output.format.dng"]);
|
|
100
102
|
/**
|
|
@@ -665,6 +667,53 @@ export function parseDisplayViewOption(value, frameWidth, frameHeight) {
|
|
|
665
667
|
}
|
|
666
668
|
return { width, height, samplesPerAxis, bitDepth: format === "srgb-8" ? 8 : 16, format: format, ...(region ? { region } : {}) };
|
|
667
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
|
+
}
|
|
668
717
|
/** Internal: passed as the view of a "display-view" run to resolve the request without a view (`prepareDisplayView`, #387). */
|
|
669
718
|
export const DISPLAY_VIEW_WITHOUT_VIEW = Symbol("display view without view");
|
|
670
719
|
/**
|
|
@@ -701,7 +750,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
701
750
|
preset = parseCallerCameraPreset(cameraOption);
|
|
702
751
|
const body = preset.body, lens = preset.lens;
|
|
703
752
|
// Lens
|
|
704
|
-
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"]);
|
|
705
754
|
// Generic prime (2.3): resolved on the simulated frame's size, which the output and sensor sections validate again below.
|
|
706
755
|
let prime;
|
|
707
756
|
if (lensOptions.prime !== undefined) {
|
|
@@ -734,7 +783,9 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
734
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.");
|
|
735
784
|
let pupil;
|
|
736
785
|
if (lensOptions.pupilSampling === undefined)
|
|
737
|
-
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).");
|
|
738
789
|
else {
|
|
739
790
|
const p = allowKeys(lensOptions.pupilSampling, "lens.pupilSampling", ["radialSamples", "angularSamples"]);
|
|
740
791
|
// The environment pupil contract admits 4..64 azimuthal midpoints and at most 256 rays.
|
|
@@ -800,6 +851,18 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
800
851
|
if (pupil)
|
|
801
852
|
fail("lens.diffraction cannot be combined with lens.pupilSampling: the environment contract does not mix a sampled PSF with pupil rays.");
|
|
802
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
|
+
}
|
|
803
866
|
// Motion (2.0): constant camera rotation and the shutter type.
|
|
804
867
|
const motionOptions = allowKeys(top.motion, "motion", ["angularVelocityRadPerSec", "shutter", "rollingShutterSkewSeconds"]);
|
|
805
868
|
const angular = { pitch: 0, yaw: 0, roll: 0 };
|
|
@@ -915,7 +978,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
915
978
|
if (displayView) {
|
|
916
979
|
const unsupported = (option, reason) => fail("simulateDisplayView does not support " + option + ": " + reason);
|
|
917
980
|
if (pupil)
|
|
918
|
-
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.");
|
|
919
982
|
if (lensFieldMapping)
|
|
920
983
|
unsupported("lens.projection or lens.distortion", "the expected signal assumes the ideal rectilinear, distortion-free mapping of the site lattice.");
|
|
921
984
|
if (vignetting)
|
|
@@ -1066,10 +1129,14 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1066
1129
|
const illuminant = resolveCaptureIlluminant(illuminantOption);
|
|
1067
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");
|
|
1068
1131
|
const irradianceScale = calculateIlluminantIrradianceScale(illuminant.shape, illuminanceLux);
|
|
1069
|
-
let chart, callerScene;
|
|
1132
|
+
let chart, callerScene, materialLayers;
|
|
1070
1133
|
const sceneRecord = sceneOption;
|
|
1071
1134
|
if ("kind" in sceneRecord) {
|
|
1072
|
-
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);
|
|
1073
1140
|
}
|
|
1074
1141
|
else {
|
|
1075
1142
|
chart = resolveChartScene({ scene: sceneOption, aspect: chartSize ? chartSize.heightM / chartSize.widthM : aspect });
|
|
@@ -1192,6 +1259,19 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1192
1259
|
fNumber = resolution.resolvedSettings.aperture ?? fNumber;
|
|
1193
1260
|
shutterSeconds = resolution.resolvedSettings.shutterSeconds ?? shutterSeconds;
|
|
1194
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;
|
|
1195
1275
|
// Sensor readout for the ISO setting. Standard output sensitivity S = 10/H (CIPA DC-004 §3.2) is defined at the
|
|
1196
1276
|
// output, independent of pixel size, while the collected charge is proportional to the sensitive-aperture area
|
|
1197
1277
|
// (EMVA 1288 Release 4.0, Eq. 3). The tier's signal chain is anchored to the 6 µm aperture; at another area the
|
|
@@ -1407,7 +1487,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1407
1487
|
uncertainty: { kind: "not-quantified", limitation: "Analytic planar scene." },
|
|
1408
1488
|
fidelity: { spectral: "wavelength-resolved", material: "spectral-data", visibility: "resolved", directTransport: "approximation", indirectTransport: "not-modeled" },
|
|
1409
1489
|
wavelengthChangingTransportModeled: false, volumetricTransportModeled: false, polarizationModeled: false, evidence, limitations: ["Engine-authored planar chart or caller scene function.",
|
|
1410
|
-
...(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)."] : [])] }
|
|
1411
1492
|
};
|
|
1412
1493
|
const lensId = lensExposure.profileId;
|
|
1413
1494
|
const opticsProfile = { schemaVersion: "0.1.0", profileId: lensId + ":facade-transmission", profileVersion: SIMULATE_CAPTURE_FACADE_VERSION, lensProfileId: lensId,
|
|
@@ -1430,9 +1511,11 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1430
1511
|
maximumSubintervalWidthNanometers: spectralStep }]));
|
|
1431
1512
|
// Diffraction (2.1): Airy kernels at the resolved f-number on the site pitch, one per spectral node of the sensor quadrature.
|
|
1432
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);
|
|
1433
1517
|
if (diffractionRadiusOption !== undefined) {
|
|
1434
|
-
const wavelengths =
|
|
1435
|
-
spectralResponseProfile: spectralResponseProfiles[channelId] }).value.nodes.map((node) => node.wavelengthNanometers)))].sort((a, b) => a - b);
|
|
1518
|
+
const wavelengths = quadratureWavelengths();
|
|
1436
1519
|
const lambdaN = wavelengths[wavelengths.length - 1] / 1000 * fNumber * imageDistanceMm / focalLengthMm;
|
|
1437
1520
|
// The environment resolves the PSF at each pre-filter source point, up to the largest split offset outside the imaging area.
|
|
1438
1521
|
const aaReachMm = antiAliasing ? Math.max(...antiAliasing.components.map((c) => Math.max(Math.abs(c.offsetMicrometers.x), Math.abs(c.offsetMicrometers.y)))) / 1000 : 0;
|
|
@@ -1463,9 +1546,36 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1463
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. ") +
|
|
1464
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."));
|
|
1465
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
|
+
}
|
|
1466
1574
|
const psf = lensPsf ?? (diffractionObjectSpace ? { kind: "not-applied", evidence,
|
|
1467
1575
|
limitation: "No environment PSF: lens diffraction is applied upstream by the scene provider as an object-space convolution (request.diffraction)." } : undefined) ??
|
|
1468
|
-
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)." };
|
|
1469
1579
|
const pupilRecord = pupil ? { kind: "ideal-uniform-circular-pupil", radialSampleCount: pupil.radialSamples, angularSampleCount: pupil.angularSamples, evidence,
|
|
1470
1580
|
limitation: "Ideal uniform circular pupil; finite ray sampling, no convergence claim." } : undefined;
|
|
1471
1581
|
const chargeSamplingProfile = { schemaVersion: "0.1.0", profileId: IDS.chargeSampling, completenessProfileId: IDS.completeness, scientificStatus: "approximation",
|
|
@@ -1502,14 +1612,19 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1502
1612
|
const readTile = async (request) => ({ ...request, sites: Array.from({ length: request.width }, (_, j) => siteInput(request.y * nativeWidth + request.x + j)) });
|
|
1503
1613
|
const yieldControl = createHostYield().yieldControl;
|
|
1504
1614
|
const perSite = queriesPerSite * (diffraction && psf === diffractionPsf ? (2 * diffraction.kernelRadiusSamples + 1) ** 2 : 1);
|
|
1505
|
-
let diffractionMs = 0, demosaicMs, encodeMs;
|
|
1506
|
-
const diffractedProvider = (window) =>
|
|
1507
|
-
wavelengthsNm:
|
|
1508
|
-
sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit, onConvolution: (ms) => {
|
|
1509
|
-
// 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; },
|
|
1510
1619
|
...(window ? { window } : {}),
|
|
1511
|
-
|
|
1512
|
-
|
|
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;
|
|
1513
1628
|
// Built on first use: a capture that takes the tabulated path (#394 step 2) never queries it, and its kernels cost a setup.
|
|
1514
1629
|
let rawProviderBuilt;
|
|
1515
1630
|
const rawProvider = () => (rawProviderBuilt ??= diffractedProvider(regionNative));
|
|
@@ -1584,7 +1699,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1584
1699
|
// Display view: the renderer that simulate-display-view.ts passes in reads this request's resolved state, so a bundle
|
|
1585
1700
|
// that only captures does not contain the display view.
|
|
1586
1701
|
if (displayView)
|
|
1587
|
-
return await renderView({ ahd, fastReport, 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 });
|
|
1588
1703
|
// Ideal reference (2.3): the scene without camera effects, at the capture's own exposure, white balance and encoding.
|
|
1589
1704
|
if (idealReference) {
|
|
1590
1705
|
const referenceStarted = clockMs();
|
|
@@ -1709,12 +1824,20 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1709
1824
|
const { electronsPerRadiance, wavelengths } = axisChannelResponse("simulateCapture");
|
|
1710
1825
|
const perNode = CHANNELS.map((c) => Float64Array.from(wavelengths, (nm) => electronsPerRadiance[c].get(nm) ?? 0));
|
|
1711
1826
|
const sceneMaterials = chart ? chart.materials : callerScene.materials, materialRadiance = provider.materialRadiance;
|
|
1712
|
-
const { signalInput } = buildExpectedSignalInput({ nativeWidth, nativeHeight, width, wavelengths, perNode, apertureNodes: apertureNodeOffsets(boxSamples), boxSamples, antiAliasing, pitch,
|
|
1713
|
-
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,
|
|
1714
1829
|
materials: { materialAt: provider.chartMaterialAt, whiteRadiance: (nm) => irradianceScale * illuminant.shape.evaluate(nm) / Math.PI,
|
|
1715
1830
|
list: [...sceneMaterials.map((m, index) => (m.kind === "neutral" ? { neutral: m.reflectance } : { radiance: (nm) => materialRadiance(index, nm) })),
|
|
1716
1831
|
...(chartSize ? [{ neutral: chartSize.surroundReflectance }] : [])] },
|
|
1717
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
|
+
}
|
|
1718
1841
|
// The verification reads only the region's top-left 2 × 2 block, so its diffraction convolves only that block's lattice; the
|
|
1719
1842
|
// window never changes a value (see diffraction-provider.ts), so the chain it runs is the whole render's.
|
|
1720
1843
|
const verificationProvider = diffractedProvider({ x: regionNative?.x ?? 0, y: regionNative?.y ?? 0, width: 2, height: 2 });
|
|
@@ -1926,7 +2049,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1926
2049
|
}
|
|
1927
2050
|
const ranOptions = new Set([...(pupil ? ["lens.pupilSampling"] : []), ...(projection !== undefined ? ["lens.projection"] : []),
|
|
1928
2051
|
...(distortion ? ["lens.distortion"] : []), ...(vignetting ? ["lens.vignetting"] : []), ...(lensPsf ? ["lens.psf"] : []),
|
|
1929
|
-
...(diffraction ? ["lens.diffraction"] : []), ...(antiAliasing ? ["sensor.antiAliasing"] : []), ...(boxAperture ? ["sensor.pixelAperture.box"] : []),
|
|
2052
|
+
...(diffraction ? ["lens.diffraction"] : []), ...(defocusSiteKernels ? ["lens.defocus"] : []), ...(antiAliasing ? ["sensor.antiAliasing"] : []), ...(boxAperture ? ["sensor.pixelAperture.box"] : []),
|
|
1930
2053
|
...(temporalSampleCount > 1 ? ["sensor.temporalSampleCount"] : []), ...(rotating ? ["motion.rotation"] : []),
|
|
1931
2054
|
...(rollingSchedule ? ["motion.shutter.rolling"] : []), ...(mirrorBorder ? ["output.borderHandling.mirror"] : []), ...(region ? ["output.region"] : []),
|
|
1932
2055
|
...(needsAuto ? ["output.whiteBalance.auto"] : []), ...(ahd ? ["output.demosaic.ahd"] : []),
|
|
@@ -1946,7 +2069,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1946
2069
|
framedWidthM: 2 * metresPerFrameUnit, framedHeightM: 2 * metresPerFrameUnit * aspect, ...(chartReport ? { chart: chartReport } : {}), sourceSha256 },
|
|
1947
2070
|
diagnostics: { providerEvaluationCount, highlight: developed.highlight, rendering: developed.diagnostics, ...(gamutMappingDiagnostics ? { gamutMapping: gamutMappingDiagnostics } : {}),
|
|
1948
2071
|
timingsMs: { raw: rawMs, whiteBalance: wbMs, development: devMs, jpeg: jpegMs, total: Date.now() - started,
|
|
1949
|
-
...(diffraction && diffractionObjectSpace ? { diffraction: diffractionMs } : {}), ...(demosaicMs === undefined ? {} : { demosaic: demosaicMs }),
|
|
2072
|
+
...(diffraction && diffractionObjectSpace && !defocusSiteKernels ? { diffraction: diffractionMs } : {}), ...(defocusSiteKernels ? { defocus: defocusMs } : {}), ...(demosaicMs === undefined ? {} : { demosaic: demosaicMs }),
|
|
1950
2073
|
...(encodeMs === undefined ? {} : { encode: encodeMs }) },
|
|
1951
2074
|
options: Object.freeze(SIMULATE_CAPTURE_COST_OPTIONS.filter((option) => ranOptions.has(option))), fast: fastReport },
|
|
1952
2075
|
provenance: { scientificStatus: "approximation", cameraPresetId: preset.presetId, cameraPresetVersion: preset.presetVersion,
|
|
@@ -1954,7 +2077,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1954
2077
|
composedContracts: ["resolveGenericEquipmentExposureCapabilities", "meterRelativeExposure", "createExposureMeterTargetFromMeteringResult",
|
|
1955
2078
|
"resolve" + (mode === "manual" ? "Manual" : mode === "aperture-priority" ? "AperturePriority" : mode === "shutter-priority" ? "ShutterPriority" : "ProgramAuto") + "ExposureMode",
|
|
1956
2079
|
...(rollingSchedule ? ["resolveElectronicRollingShutterExposureSchedule", "createRollingShutterCaptureExposureSchedule"] : []),
|
|
1957
|
-
...(diffraction ? ["createDiffractionLimitedPsfProfile", ...(diffractionObjectSpace ? ["resolveLensPsfConvolutionKernels", "calculateIrradianceConvolution"] : [])] : []),
|
|
2080
|
+
...(diffraction ? ["createDiffractionLimitedPsfProfile", ...(diffractionObjectSpace ? ["resolveLensPsfConvolutionKernels", ...(defocusSiteKernels ? [] : ["calculateIrradianceConvolution"])] : [])] : []),
|
|
2081
|
+
...(defocusSiteKernels ? ["calculateDefocusCircle", "defocus disc site kernel (object-space convolution)"] : []),
|
|
1958
2082
|
...(antiAliasing ? ["resolveAntiAliasingSpatialKernel"] : []), "resolveGenericIsoSignalChain", "createNativeEnvironmentRawTask", ...(needsAuto ? ["estimateAutoWhiteBalance"] : []),
|
|
1959
2083
|
...(builtInDemosaic ? ["createBuiltInBayerDemosaicProfiles"] : []), ...(ahd ? ["createAhdDemosaicTask"] : []),
|
|
1960
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"]),
|