@bubo-squared/gyroview 0.3.1 → 0.4.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +3 -3
  3. package/dist/index.d.ts +36 -0
  4. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +49 -15
  5. package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +3 -1
  6. package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +2 -2
  7. package/dist/packages/adapters/mediabunny/src/trackColour.js +21 -0
  8. package/dist/packages/adapters/mse-audio/src/SourceBufferFeeder.js +12 -1
  9. package/dist/packages/adapters/three/src/ThreeFrameRenderer.js +3 -0
  10. package/dist/packages/adapters/three/src/conversionUniforms.js +61 -0
  11. package/dist/packages/adapters/three/src/glslLiterals.js +10 -0
  12. package/dist/packages/adapters/three/src/matrixCorrections.js +35 -0
  13. package/dist/packages/adapters/three/src/rendererUniforms.js +33 -12
  14. package/dist/packages/adapters/three/src/shaderPrograms.js +4 -2
  15. package/dist/packages/adapters/three/src/shaders/analysis.frag.js +1 -1
  16. package/dist/packages/adapters/three/src/shaders/displayConversion.js +4 -0
  17. package/dist/packages/adapters/three/src/shaders/lensModels.js +1 -1
  18. package/dist/packages/adapters/three/src/shaders/lensSampling.js +1 -1
  19. package/dist/packages/adapters/three/src/shaders/rawLenses.frag.js +1 -1
  20. package/dist/packages/adapters/three/src/shaders/stitch.frag.js +1 -1
  21. package/dist/packages/adapters/three/src/threeMatrix.js +10 -0
  22. package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +4 -2
  23. package/dist/packages/adapters/webcodecs/src/colorSpaceOf.js +32 -0
  24. package/dist/packages/core/src/application/recording/frameTimesOf.js +0 -1
  25. package/dist/packages/core/src/domain/colour/DisplayConversion.js +137 -0
  26. package/dist/packages/core/src/domain/colour/TrackColour.js +38 -0
  27. package/dist/packages/core/src/domain/colour/matrixCorrection.js +76 -0
  28. package/dist/packages/core/src/domain/format/calibration/CalibrationVersion.js +7 -4
  29. package/dist/packages/core/src/domain/format/calibration/layouts/CalibrationStringLayout.js +1 -1
  30. package/dist/packages/core/src/domain/format/calibration/layouts/LegacyCalibrationLayout.js +1 -0
  31. package/dist/packages/core/src/domain/format/calibration/layouts/MeiCalibrationLayout.js +109 -35
  32. package/dist/packages/core/src/domain/format/calibration/layouts/PolynomialCalibrationLayout.js +1 -0
  33. package/dist/packages/core/src/domain/format/calibration/offsetTokens.js +33 -4
  34. package/dist/packages/core/src/domain/format/calibration/parseOffsetString.js +22 -17
  35. package/dist/packages/core/src/domain/format/calibration/selectCalibration.js +28 -15
  36. package/dist/packages/core/src/domain/format/calibration/v6TermReading.js +20 -0
  37. package/dist/packages/core/src/domain/format/info/calibrationSources.js +27 -0
  38. package/dist/packages/core/src/domain/format/info/infoFields.js +8 -2
  39. package/dist/packages/core/src/domain/format/info/parseInfoRecord.js +11 -6
  40. package/dist/packages/core/src/domain/motion/imu/ImuFrame.js +31 -9
  41. package/dist/packages/core/src/domain/motion/timing/FrameTimes.js +1 -3
  42. package/dist/packages/core/src/domain/motion/timing/frameTimeSources.js +0 -3
  43. package/dist/packages/core/src/domain/optics/MeiDistortion.js +43 -0
  44. package/dist/packages/core/src/domain/optics/MeiModel.js +6 -14
  45. package/dist/packages/core/src/domain/optics/scaledProjection.js +30 -0
  46. package/dist/packages/core/src/domain/stitching/StitchingSetup.js +13 -4
  47. package/dist/packages/core/src/index.js +6 -2
  48. package/dist/packages/core/src/shared/index.js +11 -0
  49. package/dist/packages/core/src/shared/mapRecord.js +11 -0
  50. package/dist/packages/core/src/shared/protobuf/ProtobufMessage.js +12 -26
  51. package/dist/packages/core/src/shared/protobuf/wireFormat.js +16 -0
  52. package/dist/packages/player/src/composition/buildPipeline.js +2 -1
  53. package/dist/packages/player/src/composition/openInputs.js +4 -1
  54. package/dist/standalone.js +211 -100
  55. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -3,6 +3,51 @@
3
3
  What changed for a page using the package, newest first. Until 1.0, a minor version may change
4
4
  the API.
5
5
 
6
+ ## 0.4.1 (2026-10-01)
7
+
8
+ What a page may notice:
9
+
10
+ - An Insta360 X4 Air recording is stabilized about the right axes: its IMU frame is measured
11
+ (ADR 0009) where it was assumed. The `ready` event's `imuFrame` is
12
+ `{ name: 'X4 Air', isVerified: true }` where it was the unverified aligned frame, and the load
13
+ no longer warns (`recording-degraded`) that the IMU frame has not been verified on a recording.
14
+ - A camera is given a measured IMU frame only by its whole model name: a model whose name merely
15
+ begins with a measured camera's, such as a later "X5 Pro", gets the unverified frame and its
16
+ warning until a recording of it is measured.
17
+
18
+ ## 0.4.0 (2026-10-01)
19
+
20
+ New:
21
+
22
+ - Insta360 X6 recordings play. Their only calibration is a v6 string, now read as the Mei model
23
+ with more terms (ADR 0032); the X6's IMU frame and the radial scale its lenses want are
24
+ measured (ADR 0009, ADR 0023); and its 10-bit HLG video is shown as SDR, as Insta360 Studio
25
+ shows it (ADR 0033).
26
+ - Each of the `ready` event's `tracks` has a `colour`: its primaries, transfer, matrix and range
27
+ as the track's bitstream says (`TrackColour`, with the types `ColourPrimaries`,
28
+ `TransferCharacteristics`, `MatrixCoefficients` and `ColourRange`).
29
+ - `inspectRecording`'s calibration strings include `offsetV6`, and `calibrationVersion` may be
30
+ 6, the v6 string's.
31
+
32
+ What a page may notice:
33
+
34
+ - A recording whose only calibration is a v6 string plays where it failed with
35
+ `no-calibration`.
36
+ - Stabilization samples the gyro half way through each frame's shutter, no longer half a
37
+ readout later. An X5's stabilized picture shifts by that half readout (4 ms at 5.7K60, 10.6 ms
38
+ at 8K30), and a swinging camera's world holds stiller (ADR 0034).
39
+ - A track whose colour the player cannot show as it should is drawn with a
40
+ `recording-degraded` warning: a PQ or linear-light transfer is drawn as recorded, SDR of wider
41
+ primaries than BT.709's is shown as BT.709, HLG of primaries other than BT.709's or BT.2020's
42
+ keeps its gamut.
43
+ - The decoder is told the track's whole colour, not only its range (the range alone where the
44
+ browser's WebCodecs does not know one of its values).
45
+ - Where a browser's decoder converts a track's colour through another matrix than the track's
46
+ own, as Safari's does with the X6's BT.2020, the picture is brought back to the track's.
47
+ - Seeking again and again, or dragging the seek bar, no longer stops playback now and then with
48
+ `decode` ("the audio element failed (media error 3)"): the sound is handed to the browser in
49
+ whole segments, so a seek can no longer cut one in half.
50
+
6
51
  ## 0.3.1 (2026-09-30)
7
52
 
8
53
  New:
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @bubo-squared/gyroview
2
2
 
3
- Play raw Insta360 `.insv` recordings (X3, X4, X5) in the browser. `<gyro-view>` reads the
3
+ Play raw Insta360 `.insv` recordings (X3, X4, X5, X6) in the browser. `<gyro-view>` reads the
4
4
  camera's dual-fisheye file directly, over HTTP byte ranges or from a local file, decodes both
5
5
  lenses in hardware with WebCodecs, and stitches and gyro-stabilizes them on the GPU. No Insta360
6
6
  Studio export step.
@@ -45,10 +45,10 @@ A page without a bundler loads the standalone file, which has Three.js and media
45
45
  ```html
46
46
  <script
47
47
  type="module"
48
- src="https://cdn.jsdelivr.net/npm/@bubo-squared/gyroview@0.3/dist/standalone.js"
48
+ src="https://cdn.jsdelivr.net/npm/@bubo-squared/gyroview@0.4/dist/standalone.js"
49
49
  ></script>
50
50
  <script type="module">
51
- import { inspectRecording } from 'https://cdn.jsdelivr.net/npm/@bubo-squared/gyroview@0.3/dist/standalone.js';
51
+ import { inspectRecording } from 'https://cdn.jsdelivr.net/npm/@bubo-squared/gyroview@0.4/dist/standalone.js';
52
52
  </script>
53
53
  ```
54
54
 
package/dist/index.d.ts CHANGED
@@ -42,6 +42,7 @@ declare const CalibrationVersion: {
42
42
  readonly Legacy: 1;
43
43
  readonly Polynomial: 2;
44
44
  readonly Mei: 3;
45
+ readonly ExtendedMei: 6;
45
46
  };
46
47
  type CalibrationVersion = (typeof CalibrationVersion)[keyof typeof CalibrationVersion];
47
48
  type Milliseconds = Brand<number, "Milliseconds">;
@@ -67,6 +68,7 @@ interface CalibrationStrings {
67
68
  readonly offset: string | undefined;
68
69
  readonly offsetV2: string | undefined;
69
70
  readonly offsetV3: string | undefined;
71
+ readonly offsetV6: string | undefined;
70
72
  }
71
73
  /**
72
74
  * Everything the player learns from the info record. Absent fields are `undefined`; nothing here
@@ -225,6 +227,39 @@ export interface RecordingInspection {
225
227
  readonly gyro: GyroSummary | UnreadableGyro | undefined;
226
228
  readonly exposure: ExposureReport | undefined;
227
229
  }
230
+ declare const COLOUR_PRIMARIES: readonly [
231
+ "bt709",
232
+ "bt470bg",
233
+ "smpte170m",
234
+ "bt2020",
235
+ "smpte432"
236
+ ];
237
+ declare const TRANSFER_CHARACTERISTICS: readonly [
238
+ "bt709",
239
+ "smpte170m",
240
+ "iec61966-2-1",
241
+ "linear",
242
+ "pq",
243
+ "hlg"
244
+ ];
245
+ declare const MATRIX_COEFFICIENTS: readonly [
246
+ "rgb",
247
+ "bt709",
248
+ "bt470bg",
249
+ "smpte170m",
250
+ "bt2020-ncl"
251
+ ];
252
+ type Unspecified = "unspecified";
253
+ export type ColourPrimaries = (typeof COLOUR_PRIMARIES)[number] | Unspecified;
254
+ export type TransferCharacteristics = (typeof TRANSFER_CHARACTERISTICS)[number] | Unspecified;
255
+ export type MatrixCoefficients = (typeof MATRIX_COEFFICIENTS)[number] | Unspecified;
256
+ export type ColourRange = "full" | "limited" | Unspecified;
257
+ export interface TrackColour {
258
+ readonly primaries: ColourPrimaries;
259
+ readonly transfer: TransferCharacteristics;
260
+ readonly matrix: MatrixCoefficients;
261
+ readonly range: ColourRange;
262
+ }
228
263
  interface VideoTrackDescription {
229
264
  readonly trackIndex: number;
230
265
  readonly codedWidth: number;
@@ -233,6 +268,7 @@ interface VideoTrackDescription {
233
268
  * WebCodecs codec string, for example `hvc1.1.6.L153.B0` or `avc1.640033`.
234
269
  */
235
270
  readonly codec: string;
271
+ readonly colour: TrackColour;
236
272
  }
237
273
  export type StabilizationMode = "off" | "lock" | "horizon" | "follow";
238
274
  export declare const STABILIZATION_MODES: readonly StabilizationMode[];
@@ -1,4 +1,4 @@
1
- import { GyroViewError } from "../../../core/src/shared/errors/GyroViewError.js";
1
+ import { GyroViewError, ensureInvariant } from "../../../core/src/shared/errors/GyroViewError.js";
2
2
  import "../../../core/src/index.js";
3
3
  import { SegmentChannel } from "./SegmentChannel.js";
4
4
  import { EncodedAudioPacketSource, EncodedPacket, Mp4OutputFormat, NullTarget, Output } from "mediabunny";
@@ -9,10 +9,9 @@ import { EncodedAudioPacketSource, EncodedPacket, Mp4OutputFormat, NullTarget, O
9
9
  */
10
10
  var FRAGMENT_DURATION_SECONDS = 1;
11
11
  /**
12
- * Segments (each fragment is two: `moof` and `mdat`) that may wait for the consumer before
13
- * re-packaging pauses.
12
+ * Media segments that may wait for the consumer before re-packaging pauses.
14
13
  */
15
- var SEGMENTS_AHEAD = 4;
14
+ var SEGMENTS_AHEAD = 2;
16
15
  /**
17
16
  * The codec strings of the audio the cameras record, AAC (`mp4a.40.2` for AAC-LC), and mediabunny's
18
17
  * name for it. Other codecs are not re-packaged: the picture then plays on a silent clock.
@@ -21,10 +20,10 @@ var AAC_CODEC_STRING_PREFIX = "mp4a.40.";
21
20
  var AAC = "aac";
22
21
  /**
23
22
  * AudioPackager over mediabunny: the samples re-packaged, unchanged, into fragmented MP4.
24
- * Segments are taken from the muxer's box callbacks rather than from its byte stream: that
25
- * yields whole `ftyp`/`moov`/`moof`/`mdat` boxes and leaves out the `mfra` index the muxer
26
- * appends at the end, which Media Source Extensions do not accept. Fragmented output keeps the
27
- * track's own timestamps, so segments started mid-track land at their true time.
23
+ * Segments are assembled from the muxer's box callbacks rather than taken from its byte stream:
24
+ * that yields whole segments and leaves out the `mfra` index the muxer appends at the end, which
25
+ * Media Source Extensions do not accept. Fragmented output keeps the track's own timestamps, so
26
+ * segments started mid-track land at their true time.
28
27
  */
29
28
  var MediabunnyAudioPackager = class {
30
29
  segmentsOf(samples, configuration) {
@@ -112,18 +111,53 @@ function decoderConfigOf(configuration) {
112
111
  ...configuration.description && { description: configuration.description }
113
112
  };
114
113
  }
114
+ /**
115
+ * The muxer's boxes handed on as whole segments: the initialization segment (`ftyp` and `moov`)
116
+ * and each media segment (`moof` and its `mdat`). A consumer that stops between two segments, as
117
+ * a seek does, then never leaves a source buffer's parser inside one, where Chromium reads the
118
+ * next segments' bytes as the samples of the `moof` it holds and fails to decode them.
119
+ */
115
120
  function fragmentedMp4Into(channel) {
116
- const forward = (data) => {
117
- channel.push(new Uint8Array(data));
118
- };
121
+ const segment = new SegmentAssembly(channel);
119
122
  return new Mp4OutputFormat({
120
123
  fastStart: "fragmented",
121
124
  minimumFragmentDuration: FRAGMENT_DURATION_SECONDS,
122
- onFtyp: forward,
123
- onMoov: forward,
124
- onMoof: forward,
125
- onMdat: forward
125
+ onFtyp: (data) => {
126
+ segment.begin(data);
127
+ },
128
+ onMoov: (data) => {
129
+ segment.end(data);
130
+ },
131
+ onMoof: (data) => {
132
+ segment.begin(data);
133
+ },
134
+ onMdat: (data) => {
135
+ segment.end(data);
136
+ }
126
137
  });
127
138
  }
139
+ /**
140
+ * A segment's first box held until the box that ends it arrives, then both pushed as one.
141
+ */
142
+ var SegmentAssembly = class {
143
+ channel;
144
+ opening;
145
+ constructor(channel) {
146
+ this.channel = channel;
147
+ }
148
+ begin(box) {
149
+ ensureInvariant(this.opening === void 0, "a segment began before the last one ended");
150
+ this.opening = new Uint8Array(box);
151
+ }
152
+ end(box) {
153
+ const { opening } = this;
154
+ ensureInvariant(opening !== void 0, "a segment ended that never began");
155
+ const whole = new Uint8Array(opening.byteLength + box.byteLength);
156
+ whole.set(opening);
157
+ whole.set(box, opening.byteLength);
158
+ this.opening = void 0;
159
+ this.channel.push(whole);
160
+ }
161
+ };
128
162
  //#endregion
129
163
  export { MediabunnyAudioPackager };
@@ -1,6 +1,7 @@
1
1
  import { GyroViewError, isAbortError } from "../../../core/src/shared/errors/GyroViewError.js";
2
2
  import "../../../core/src/index.js";
3
3
  import { audioConfigurationOf, videoConfigurationOf } from "./decoderConfigurations.js";
4
+ import { trackColourOf } from "./trackColour.js";
4
5
  import { ALL_FORMATS, BufferSource, Input } from "mediabunny";
5
6
  //#region ../../packages/adapters/mediabunny/src/MediabunnyCodecReader.ts
6
7
  /**
@@ -39,7 +40,8 @@ async function videoCodecOf(track, trackIndex) {
39
40
  trackIndex,
40
41
  codedWidth,
41
42
  codedHeight,
42
- codec: config.codec
43
+ codec: config.codec,
44
+ colour: trackColourOf(config.colorSpace)
43
45
  };
44
46
  return {
45
47
  trackId: track.id,
@@ -2,7 +2,7 @@ import { copyOfBytes } from "./bufferSources.js";
2
2
  //#region ../../packages/adapters/mediabunny/src/decoderConfigurations.ts
3
3
  /**
4
4
  * A WebCodecs video decoder configuration in the core's terms; the coded size comes from the
5
- * track where the configuration leaves it out.
5
+ * track where the configuration leaves it out, the colour from the track's description.
6
6
  */
7
7
  function videoConfigurationOf(config, description) {
8
8
  return {
@@ -10,7 +10,7 @@ function videoConfigurationOf(config, description) {
10
10
  codedWidth: config.codedWidth ?? description.codedWidth,
11
11
  codedHeight: config.codedHeight ?? description.codedHeight,
12
12
  description: config.description === void 0 ? void 0 : copyOfBytes(config.description),
13
- isFullRange: config.colorSpace?.fullRange ?? void 0
13
+ colour: description.colour
14
14
  };
15
15
  }
16
16
  /**
@@ -0,0 +1,21 @@
1
+ import { COLOUR_PRIMARIES, MATRIX_COEFFICIENTS, TRANSFER_CHARACTERISTICS, namedOrUnspecified } from "../../../core/src/domain/colour/TrackColour.js";
2
+ import "../../../core/src/index.js";
3
+ //#region ../../packages/adapters/mediabunny/src/trackColour.ts
4
+ /**
5
+ * A track's colour in the core's terms, from the colour space mediabunny reads off its sample
6
+ * entry or, without a `colr` box, its SPS: a value the core does not name is unspecified.
7
+ */
8
+ function trackColourOf(colorSpace) {
9
+ return {
10
+ primaries: namedOrUnspecified(COLOUR_PRIMARIES, colorSpace?.primaries),
11
+ transfer: namedOrUnspecified(TRANSFER_CHARACTERISTICS, colorSpace?.transfer),
12
+ matrix: namedOrUnspecified(MATRIX_COEFFICIENTS, colorSpace?.matrix),
13
+ range: rangeOf(colorSpace?.fullRange)
14
+ };
15
+ }
16
+ function rangeOf(fullRange) {
17
+ if (fullRange === true) return "full";
18
+ return fullRange === false ? "limited" : "unspecified";
19
+ }
20
+ //#endregion
21
+ export { trackColourOf };
@@ -73,7 +73,7 @@ var SourceBufferFeeder = class {
73
73
  async feed(from, stop) {
74
74
  try {
75
75
  await this.settlePendingAppend();
76
- if (this.hasAllAudioFrom(from)) return;
76
+ if (stop.wasStopped || this.hasElementFailed() || this.hasAllAudioFrom(from)) return;
77
77
  await this.feedFrom(this.startOf(from), stop);
78
78
  } catch (error) {
79
79
  this.failureValue ??= asGyroViewError(error, "decode", "feeding the audio buffer failed");
@@ -106,11 +106,15 @@ var SourceBufferFeeder = class {
106
106
  }
107
107
  /**
108
108
  * Appends segments as the playhead needs them. True when the track ended, false when stopped.
109
+ * A run stops only between two whole segments (the port's promise) and never cancels an
110
+ * append, so the source buffer's parser is always at a segment's start when the next run
111
+ * appends its initialization segment.
109
112
  */
110
113
  async pump(segments, stop) {
111
114
  let next = await this.nextWhenNeeded(segments, stop);
112
115
  while (next !== STOPPED) {
113
116
  if (next.done === true) return true;
117
+ if (this.hasElementFailed()) return false;
114
118
  this.evictBehind();
115
119
  await this.append(next.value);
116
120
  next = await this.nextWhenNeeded(segments, stop);
@@ -162,6 +166,13 @@ var SourceBufferFeeder = class {
162
166
  }
163
167
  if (await outcome === "error") throw new GyroViewError("decode", "the audio buffer could not parse a segment");
164
168
  }
169
+ /**
170
+ * The element failed: every append now throws, and the element's own error says why, not the
171
+ * feeder's.
172
+ */
173
+ hasElementFailed() {
174
+ return this.parts.element.error !== null;
175
+ }
165
176
  async settlePendingAppend() {
166
177
  if (this.parts.sourceBuffer.updating) await nextOfEvents(this.parts.sourceBuffer, ["updateend"]);
167
178
  }
@@ -5,6 +5,7 @@ import { aspectOf } from "../../../core/src/domain/view/screenLayout.js";
5
5
  import { DEFAULT_FRAMING } from "../../../core/src/domain/view/Framing.js";
6
6
  import { viewModeRulesFor } from "../../../core/src/domain/view/viewModes.js";
7
7
  import "../../../core/src/index.js";
8
+ import { MatrixCorrections } from "./matrixCorrections.js";
8
9
  import { SAMPLING_STRATEGIES } from "./samplingStrategies.js";
9
10
  import { applyLensGain, applyPicture, applyShaderSampling, applyStabilization, createRendererUniforms } from "./rendererUniforms.js";
10
11
  import { createFullscreenTriangle } from "./fullscreenPass.js";
@@ -127,6 +128,7 @@ var ThreeFrameRenderer = class {
127
128
  const frame = frames[index];
128
129
  if (frame) texture.setFrame(frame.handle);
129
130
  }
131
+ this.parts.matrixCorrections.follow(frames.map((frame) => frame.handle));
130
132
  this.hasFrames = true;
131
133
  this.render();
132
134
  }
@@ -250,6 +252,7 @@ function assembleParts(renderer, setup, pictures) {
250
252
  materials,
251
253
  textures,
252
254
  uniforms,
255
+ matrixCorrections: new MatrixCorrections(setup.lenses, uniforms.uLensMatrixCorrection),
253
256
  lensCount: setup.lenses.length,
254
257
  seamProof
255
258
  };
@@ -0,0 +1,61 @@
1
+ import { IDENTITY_MATRIX3 } from "../../../core/src/shared/math/Matrix3.js";
2
+ import { BT709_LUMINANCE, HLG_OETF } from "../../../core/src/domain/colour/DisplayConversion.js";
3
+ import "../../../core/src/index.js";
4
+ import { toThreeMatrix } from "./threeMatrix.js";
5
+ import { glslFloat } from "./glslLiterals.js";
6
+ import { Vector4 } from "three";
7
+ //#region ../../packages/adapters/three/src/conversionUniforms.ts
8
+ /**
9
+ * The kinds of conversion as the shader tells them apart; it shows any kind but HLG's as
10
+ * recorded.
11
+ */
12
+ var CONVERSION_AS_RECORDED = 0;
13
+ var CONVERSION_HLG_TO_SDR_BT709 = 1;
14
+ /**
15
+ * The constants `displayConversion.glsl` refers to: HLG's kind, BT.2100 HLG's constants and
16
+ * BT.709's luminance, from the core.
17
+ */
18
+ var CONVERSION_DEFINES = [
19
+ ["CONVERSION_HLG_TO_SDR_BT709", CONVERSION_HLG_TO_SDR_BT709],
20
+ ["HLG_A", glslFloat(HLG_OETF.a)],
21
+ ["HLG_B", glslFloat(HLG_OETF.b)],
22
+ ["HLG_C", glslFloat(HLG_OETF.c)],
23
+ ["HLG_SEGMENT_JOIN", glslFloat(HLG_OETF.segmentJoin)],
24
+ ["HLG_SQUARE_SEGMENT_DIVISOR", glslFloat(HLG_OETF.squareSegmentDivisor)],
25
+ ["HLG_LOG_SEGMENT_DIVISOR", glslFloat(HLG_OETF.logSegmentDivisor)],
26
+ ["BT709_LUMINANCE_RED", glslFloat(BT709_LUMINANCE[0])],
27
+ ["BT709_LUMINANCE_GREEN", glslFloat(BT709_LUMINANCE[1])],
28
+ ["BT709_LUMINANCE_BLUE", glslFloat(BT709_LUMINANCE[2])]
29
+ ];
30
+ function conversionUniforms(lenses) {
31
+ const slots = lenses.map((lens) => conversionSlotOf(lens.displayConversion));
32
+ return {
33
+ uLensConversion: { value: slots.map((slot) => slot.kind) },
34
+ uLensMatrixCorrection: { value: lenses.map(() => toThreeMatrix(IDENTITY_MATRIX3)) },
35
+ uLensGamut: { value: slots.map((slot) => slot.gamut) },
36
+ uLensTone: { value: slots.map((slot) => slot.tone) }
37
+ };
38
+ }
39
+ /**
40
+ * Exhaustive over the conversion kinds: a new kind does not compile until it is packed. What a
41
+ * kind does not use stays neutral: the identity gamut, a tone of zeros.
42
+ */
43
+ function conversionSlotOf(conversion) {
44
+ switch (conversion.kind) {
45
+ case "as-recorded": return {
46
+ kind: CONVERSION_AS_RECORDED,
47
+ gamut: toThreeMatrix(IDENTITY_MATRIX3),
48
+ tone: new Vector4()
49
+ };
50
+ case "hlg-to-sdr-bt709": {
51
+ const { exposure, kneeStart, ceiling, exponent } = conversion.tone;
52
+ return {
53
+ kind: CONVERSION_HLG_TO_SDR_BT709,
54
+ gamut: toThreeMatrix(conversion.gamut),
55
+ tone: new Vector4(exposure, kneeStart, ceiling, exponent)
56
+ };
57
+ }
58
+ }
59
+ }
60
+ //#endregion
61
+ export { CONVERSION_DEFINES, conversionUniforms };
@@ -0,0 +1,10 @@
1
+ //#region ../../packages/adapters/three/src/glslLiterals.ts
2
+ /**
3
+ * A number as a GLSL float literal, with its decimal point: three.js writes a define's number as
4
+ * JavaScript prints it, so 3 would be an int, and GLSL ES 3.00 converts no int to a float.
5
+ */
6
+ function glslFloat(value) {
7
+ return Number.isSafeInteger(value) ? value.toFixed(1) : String(value);
8
+ }
9
+ //#endregion
10
+ export { glslFloat };
@@ -0,0 +1,35 @@
1
+ import { MATRIX_COEFFICIENTS, namedOrUnspecified } from "../../../core/src/domain/colour/TrackColour.js";
2
+ import { matrixCorrectionOf } from "../../../core/src/domain/colour/matrixCorrection.js";
3
+ import "../../../core/src/index.js";
4
+ import { toThreeMatrix } from "./threeMatrix.js";
5
+ //#region ../../packages/adapters/three/src/matrixCorrections.ts
6
+ /**
7
+ * Follows the matrix each lens's frames name, and brings their texels back to the matrix the
8
+ * track was recorded with where the two differ (ADR 0033): WebKit's decoders convert Y′CbCr with
9
+ * BT.709 whatever a stream says, and name BT.709 on the frames they make.
10
+ */
11
+ var MatrixCorrections = class {
12
+ lenses;
13
+ uniform;
14
+ named;
15
+ constructor(lenses, uniform) {
16
+ this.lenses = lenses;
17
+ this.uniform = uniform;
18
+ this.named = Array.from({ length: lenses.length });
19
+ }
20
+ /**
21
+ * Updates a lens's correction when its frames name another matrix than before.
22
+ */
23
+ follow(frames) {
24
+ for (const [index, lens] of this.lenses.entries()) {
25
+ const frame = frames[lens.frameSlot];
26
+ if (!frame) continue;
27
+ const named = namedOrUnspecified(MATRIX_COEFFICIENTS, frame.colorSpace.matrix);
28
+ if (named === this.named[index]) continue;
29
+ this.named[index] = named;
30
+ this.uniform.value[index] = toThreeMatrix(matrixCorrectionOf(lens.displayConversion.matrix, named));
31
+ }
32
+ }
33
+ };
34
+ //#endregion
35
+ export { MatrixCorrections };
@@ -1,7 +1,10 @@
1
1
  import { ensureIndexInRange, ensureInvariant } from "../../../core/src/shared/errors/GyroViewError.js";
2
+ import { MEI_TERM_CAPACITY } from "../../../core/src/domain/optics/MeiDistortion.js";
2
3
  import { IDENTITY_MATRIX3 } from "../../../core/src/shared/math/Matrix3.js";
3
4
  import { aspectOfArea, planeHalfExtentOf } from "../../../core/src/domain/view/rectilinear.js";
4
5
  import "../../../core/src/index.js";
6
+ import { toThreeMatrix } from "./threeMatrix.js";
7
+ import { CONVERSION_DEFINES, conversionUniforms } from "./conversionUniforms.js";
5
8
  import "./samplingStrategies.js";
6
9
  import { Matrix3, Vector2, Vector3, Vector4 } from "three";
7
10
  var LENS_MEI = 0;
@@ -14,6 +17,10 @@ var SHADER_DEFINES = Object.fromEntries([
14
17
  ["MAX_LENSES", 2],
15
18
  ["LENS_MEI", LENS_MEI],
16
19
  ["LENS_RADIAL_POLYNOMIAL", LENS_RADIAL_POLYNOMIAL],
20
+ ["MEI_RADIAL_TERMS", MEI_TERM_CAPACITY.radial],
21
+ ["MEI_TANGENTIAL_ORDERS", MEI_TERM_CAPACITY.tangential],
22
+ ["MEI_THIN_PRISM_ORDERS", MEI_TERM_CAPACITY.thinPrism],
23
+ ...CONVERSION_DEFINES,
17
24
  ["SAMPLING_BILINEAR", 0],
18
25
  ["SAMPLING_TRILINEAR", 1],
19
26
  ["SAMPLING_SUPERSAMPLED", 2]
@@ -24,6 +31,7 @@ function createRendererUniforms(setup, textures) {
24
31
  ...viewUniforms(setup),
25
32
  ...projectionUniforms(lenses),
26
33
  ...samplingUniforms(lenses),
34
+ ...conversionUniforms(lenses),
27
35
  uTexture0: { value: textures[0] ?? null },
28
36
  uTexture1: { value: textures[1] ?? null }
29
37
  };
@@ -47,12 +55,18 @@ function projectionUniforms(lenses) {
47
55
  uLensPrincipalPoint: { value: lenses.map((lens) => toVector2(lens.projection.principalPoint)) },
48
56
  uLensFocal: { value: slots.map((slot) => slot.focal) },
49
57
  uLensXi: { value: slots.map((slot) => slot.xi) },
50
- uLensRadial: { value: slots.map((slot) => slot.radial) },
51
- uLensTangential: { value: slots.map((slot) => slot.tangential) },
58
+ uLensRadial: { value: slots.flatMap((slot) => slot.radial) },
59
+ uLensTangential: { value: slots.flatMap((slot) => slot.tangential) },
60
+ uLensThinPrism: { value: slots.flatMap((slot) => slot.thinPrism) },
52
61
  uLensPolynomial: { value: slots.map((slot) => slot.polynomial) },
53
62
  uLensHalfFov: { value: lenses.map((lens) => lens.halfFieldOfView) }
54
63
  };
55
64
  }
65
+ var NO_MEI_DISTORTION = {
66
+ radial: [],
67
+ tangential: [],
68
+ thinPrism: []
69
+ };
56
70
  /**
57
71
  * Exhaustive over the projection kinds: a new kind does not compile until it is packed.
58
72
  */
@@ -62,20 +76,33 @@ function slotOf(projection) {
62
76
  kind: LENS_MEI,
63
77
  focal: new Vector2(...projection.focal),
64
78
  xi: projection.xi,
65
- radial: new Vector3(...projection.radial),
66
- tangential: new Vector2(...projection.tangential),
79
+ ...meiTermSlots(projection.distortion),
67
80
  polynomial: new Vector4()
68
81
  };
69
82
  case "radial-polynomial": return {
70
83
  kind: LENS_RADIAL_POLYNOMIAL,
71
84
  focal: new Vector2(),
72
85
  xi: 0,
73
- radial: new Vector3(),
74
- tangential: new Vector2(),
86
+ ...meiTermSlots(NO_MEI_DISTORTION),
75
87
  polynomial: new Vector4(...projection.coefficients)
76
88
  };
77
89
  }
78
90
  }
91
+ function meiTermSlots(distortion) {
92
+ return {
93
+ radial: fittedToCapacity(distortion.radial, MEI_TERM_CAPACITY.radial, () => 0),
94
+ tangential: fittedToCapacity(distortion.tangential.map((pair) => new Vector2(pair.p1, pair.p2)), MEI_TERM_CAPACITY.tangential, () => new Vector2()),
95
+ thinPrism: fittedToCapacity(distortion.thinPrism.map((term) => new Vector2(term.x, term.y)), MEI_TERM_CAPACITY.thinPrism, () => new Vector2())
96
+ };
97
+ }
98
+ /**
99
+ * The terms in the room the shader holds for their family, the rest of it zero; more terms than
100
+ * it holds are a defect of the model that made them.
101
+ */
102
+ function fittedToCapacity(terms, capacity, zero) {
103
+ ensureInvariant(terms.length <= capacity, `a Mei lens has ${terms.length} terms of a family the shader holds ${capacity} of`);
104
+ return Array.from({ length: capacity }, (_unused, index) => terms[index] ?? zero());
105
+ }
79
106
  function samplingUniforms(lenses) {
80
107
  return {
81
108
  uLensWindow: { value: lenses.map((lens) => new Vector4(lens.window.x, lens.window.y, lens.window.width, lens.window.height)) },
@@ -136,12 +163,6 @@ function valuesOf(picture, viewportAspect) {
136
163
  function toVector4(area) {
137
164
  return new Vector4(area.x, area.y, area.width, area.height);
138
165
  }
139
- /**
140
- * Row-major core matrix into a three matrix, whose `set` takes rows too.
141
- */
142
- function toThreeMatrix(m) {
143
- return new Matrix3().set(...m);
144
- }
145
166
  function toVector2(point) {
146
167
  return new Vector2(point.x, point.y);
147
168
  }
@@ -1,4 +1,5 @@
1
1
  import analysis_frag_default from "./shaders/analysis.frag.js";
2
+ import displayConversion_default from "./shaders/displayConversion.js";
2
3
  import equirectangularRays_default from "./shaders/equirectangularRays.js";
3
4
  import fixedJoin_default from "./shaders/fixedJoin.js";
4
5
  import header_default from "./shaders/header.js";
@@ -14,8 +15,9 @@ import stitch_frag_default from "./shaders/stitch.frag.js";
14
15
  * The fragment programs, each the GLSL chunks it is made of in the order they must appear: a
15
16
  * chunk comes after every chunk whose declarations it uses. The only place that order is known.
16
17
  */
18
+ var LENS_TEXELS = [lensTextures_default, displayConversion_default];
17
19
  var LENS_PROJECTION = [
18
- lensTextures_default,
20
+ ...LENS_TEXELS,
19
21
  lensModels_default,
20
22
  lensSampling_default
21
23
  ];
@@ -48,7 +50,7 @@ function pictureProgramsWith(seamJoin) {
48
50
  "lens-tiles": [
49
51
  header_default,
50
52
  screenAreas_default,
51
- lensTextures_default,
53
+ ...LENS_TEXELS,
52
54
  rawLenses_frag_default
53
55
  ]
54
56
  };
@@ -1,4 +1,4 @@
1
1
  //#region ../../packages/adapters/three/src/shaders/analysis.frag.glsl?raw
2
- var analysis_frag_default = "in vec2 vNdc;\nout vec4 outColor;\n\n// One row per lens: what that lens sees along the seam ring, the circle of directions halfway\n// through the blend band around lens 0's optical axis, sampled around the ring along x. Rows\n// share their directions, so their means differ by exposure (and parallax), not by content.\n// Alpha marks the texels a lens actually images.\nvoid main() {\n int lens = int(floor((vNdc.y + 1.0) * 0.5 * float(MAX_LENSES)));\n float phi = (vNdc.x + 1.0) * PI;\n float ring = 0.5 * (uFeather.x + uFeather.y);\n vec3 inLensZero = vec3(sin(ring) * cos(phi), sin(ring) * sin(phi), cos(ring));\n // The lens rotations take body directions into each lens; the transpose goes back.\n vec3 dirBody = transpose(uLensRotation[0]) * inLensZero;\n if (lens >= uLensCount) {\n outColor = vec4(0.0);\n return;\n }\n LensSample seen = sampleLensWith(lens, dirBody, SAMPLING_BILINEAR);\n outColor = seen.isImaged ? vec4(seen.color, 1.0) : vec4(0.0);\n}\n";
2
+ var analysis_frag_default = "in vec2 vNdc;\nout vec4 outColor;\n\n// One row per lens: what that lens sees along the seam ring, the circle of directions halfway\n// through the blend band around lens 0's optical axis, sampled around the ring along x. Rows\n// share their directions, so their means differ by exposure (and parallax), not by content:\n// exposure signals, in which it is a factor (ADR 0033). Alpha marks the texels a lens images.\nvoid main() {\n int lens = int(floor((vNdc.y + 1.0) * 0.5 * float(MAX_LENSES)));\n float phi = (vNdc.x + 1.0) * PI;\n float ring = 0.5 * (uFeather.x + uFeather.y);\n vec3 inLensZero = vec3(sin(ring) * cos(phi), sin(ring) * sin(phi), cos(ring));\n // The lens rotations take body directions into each lens; the transpose goes back.\n vec3 dirBody = transpose(uLensRotation[0]) * inLensZero;\n if (lens >= uLensCount) {\n outColor = vec4(0.0);\n return;\n }\n LensSample seen = sampleLensWith(lens, dirBody, SAMPLING_BILINEAR);\n outColor = seen.isImaged ? vec4(seen.signal, 1.0) : vec4(0.0);\n}\n";
3
3
  //#endregion
4
4
  export { analysis_frag_default as default };
@@ -0,0 +1,4 @@
1
+ //#region ../../packages/adapters/three/src/shaders/displayConversion.glsl?raw
2
+ var displayConversion_default = "// How lens i's texels are brought to the SDR BT.709 display, as the core's exposureSignalOf and\n// shownOf (ADR 0033): the browser's upload has applied the track's matrix and range; the\n// transfer and primaries are this chunk's. Shown as recorded, or HLG converted to scene light,\n// into BT.709 and toned; gain matching scales the exposure signal between the two (ADR 0012).\nuniform int uLensConversion[MAX_LENSES];\n// R′G′B′ through the matrix a frame names back to the track's own (matrixCorrectionOf).\nuniform mat3 uLensMatrixCorrection[MAX_LENSES];\nuniform mat3 uLensGamut[MAX_LENSES];\n// Exposure, roll-off start, roll-off ceiling, display encoding power.\nuniform vec4 uLensTone[MAX_LENSES];\n\nstruct ToneCurve {\n float exposure;\n float kneeStart;\n float ceiling;\n float exponent;\n};\n\nToneCurve toneOf(int i) {\n vec4 tone = uLensTone[i];\n return ToneCurve(tone.x, tone.y, tone.z, tone.w);\n}\n\nfloat hlgInverseOetf(float signal) {\n return signal <= HLG_SEGMENT_JOIN\n ? signal * signal / HLG_SQUARE_SEGMENT_DIVISOR\n : (exp((signal - HLG_C) / HLG_A) + HLG_B) / HLG_LOG_SEGMENT_DIVISOR;\n}\n\nconst vec3 BT709_LUMINANCE = vec3(BT709_LUMINANCE_RED, BT709_LUMINANCE_GREEN, BT709_LUMINANCE_BLUE);\n\nfloat rolledOff(float luminance, ToneCurve tone) {\n float over = luminance - tone.kneeStart;\n return over <= 0.0\n ? luminance\n : tone.kneeStart + over / (1.0 + over / (tone.ceiling - tone.kneeStart));\n}\n\n// The signal exposed back to light, its luminance rolled off with every channel scaled alike,\n// and encoded for the display.\nvec3 tonedFrom(vec3 signal, ToneCurve tone) {\n vec3 light = tone.exposure * pow(signal, vec3(tone.exponent));\n float luminance = dot(light, BT709_LUMINANCE);\n float scale = luminance > 0.0 ? rolledOff(luminance, tone) / luminance : 1.0;\n return min(pow(light * scale, vec3(1.0 / tone.exponent)), vec3(1.0));\n}\n\nbool isHlg(int i) {\n return uLensConversion[i] == CONVERSION_HLG_TO_SDR_BT709;\n}\n\n// Lens i's texel where its exposure is a factor; as recorded for any kind but HLG.\nvec3 exposureSignalOf(int i, vec3 texel) {\n vec3 recorded = uLensMatrixCorrection[i] * texel;\n if (!isHlg(i)) return recorded;\n // A corrected texel may fall just below black, which the square segment would make light.\n recorded = max(recorded, 0.0);\n vec3 scene = vec3(\n hlgInverseOetf(recorded.r),\n hlgInverseOetf(recorded.g),\n hlgInverseOetf(recorded.b)\n );\n vec3 light = max(uLensGamut[i] * scene, 0.0);\n return pow(light, vec3(1.0 / toneOf(i).exponent));\n}\n\n// An exposure signal of lens i as the display shows it; as recorded for any kind but HLG.\nvec3 shownOf(int i, vec3 signal) {\n if (!isHlg(i)) return signal;\n return tonedFrom(signal, toneOf(i));\n}\n\nvec3 toDisplay(int i, vec3 texel) {\n return shownOf(i, exposureSignalOf(i, texel));\n}\n";
3
+ //#endregion
4
+ export { displayConversion_default as default };
@@ -1,4 +1,4 @@
1
1
  //#region ../../packages/adapters/three/src/shaders/lensModels.glsl?raw
2
- var lensModels_default = "// The depth is positive for every direction inside a lens's field; directions behind it are\n// projected too, for the footprint's derivatives, and must not divide by zero.\nconst float MIN_DEPTH = 1e-3;\n\n// Canvas pixel of a unit direction in the lens frame under the unified (Mei) model: project onto\n// the unit sphere, then from xi behind its centre onto the image plane, distort, scale.\nvec2 projectMei(vec3 d, float xi, vec2 focal, vec2 principalPoint, vec3 radial, vec2 tangential) {\n float depth = max(d.z + xi, MIN_DEPTH);\n vec2 m = d.xy / depth;\n float r2 = dot(m, m);\n float radialFactor = 1.0 + radial.x * r2 + radial.y * r2 * r2 + radial.z * r2 * r2 * r2;\n vec2 tangentialTerm = vec2(\n 2.0 * tangential.x * m.x * m.y + tangential.y * (r2 + 2.0 * m.x * m.x),\n tangential.x * (r2 + 2.0 * m.y * m.y) + 2.0 * tangential.y * m.x * m.y\n );\n return focal * (radialFactor * m + tangentialTerm) + principalPoint;\n}\n\n// Canvas pixel of a unit direction under a radial polynomial in the angle from the optical axis.\nvec2 projectRadialPolynomial(vec3 d, float theta, vec4 c, vec2 principalPoint) {\n float radius = theta * (c.x + theta * (c.y + theta * (c.z + theta * c.w)));\n float lateral = length(d.xy);\n vec2 unit = lateral > 0.0 ? d.xy / lateral : vec2(0.0);\n return principalPoint + radius * unit;\n}\n";
2
+ var lensModels_default = "// The depth is positive for every direction inside a lens's field; directions behind it are\n// projected too, for the footprint's derivatives, and must not divide by zero.\nconst float MIN_DEPTH = 1e-3;\n\nuniform vec2 uLensPrincipalPoint[MAX_LENSES];\nuniform vec2 uLensFocal[MAX_LENSES];\nuniform float uLensXi[MAX_LENSES];\n// Each Mei lens's distortion as the core's MeiDistortion holds it, lens after lens, as many terms\n// per lens as each family holds room for, by increasing order: radial terms k1, k2, ... of r²,\n// Brown's tangential pairs (p1, p2) scaled by r^(2j), and thin-prism shifts scaled by r^(2j + 2).\n// The orders a lens does not fill are zero.\nuniform float uLensRadial[MAX_LENSES * MEI_RADIAL_TERMS];\nuniform vec2 uLensTangential[MAX_LENSES * MEI_TANGENTIAL_ORDERS];\nuniform vec2 uLensThinPrism[MAX_LENSES * MEI_THIN_PRISM_ORDERS];\nuniform vec4 uLensPolynomial[MAX_LENSES];\n\n// c0 + c1 r² + c2 r⁴ + ... of each of lens i's families, by Horner's rule, as the core's seriesIn.\nfloat radialSeries(int i, float r2) {\n float sum = 0.0;\n for (int j = MEI_RADIAL_TERMS - 1; j >= 0; j--) {\n sum = sum * r2 + uLensRadial[i * MEI_RADIAL_TERMS + j];\n }\n return sum;\n}\n\nvec2 tangentialSeries(int i, float r2) {\n vec2 sum = vec2(0.0);\n for (int j = MEI_TANGENTIAL_ORDERS - 1; j >= 0; j--) {\n sum = sum * r2 + uLensTangential[i * MEI_TANGENTIAL_ORDERS + j];\n }\n return sum;\n}\n\nvec2 thinPrismSeries(int i, float r2) {\n vec2 sum = vec2(0.0);\n for (int j = MEI_THIN_PRISM_ORDERS - 1; j >= 0; j--) {\n sum = sum * r2 + uLensThinPrism[i * MEI_THIN_PRISM_ORDERS + j];\n }\n return sum;\n}\n\n// A normalised image-plane point distorted by lens i, as the core's distortMei.\nvec2 distortMei(int i, vec2 m) {\n float r2 = dot(m, m);\n float radialFactor = 1.0 + r2 * radialSeries(i, r2);\n vec2 decentering = tangentialSeries(i, r2);\n vec2 tangentialTerm = vec2(\n 2.0 * decentering.x * m.x * m.y + decentering.y * (r2 + 2.0 * m.x * m.x),\n decentering.x * (r2 + 2.0 * m.y * m.y) + 2.0 * decentering.y * m.x * m.y\n );\n return radialFactor * m + tangentialTerm + r2 * thinPrismSeries(i, r2);\n}\n\n// Canvas pixel of a unit direction in lens i's frame under the unified (Mei) model: project onto\n// the unit sphere, then from xi behind its centre onto the image plane, distort, scale.\nvec2 projectMei(int i, vec3 d) {\n float depth = max(d.z + uLensXi[i], MIN_DEPTH);\n return uLensFocal[i] * distortMei(i, d.xy / depth) + uLensPrincipalPoint[i];\n}\n\n// Canvas pixel of a unit direction under lens i's radial polynomial in the angle from its axis.\nvec2 projectRadialPolynomial(int i, vec3 d, float theta) {\n vec4 c = uLensPolynomial[i];\n float radius = theta * (c.x + theta * (c.y + theta * (c.z + theta * c.w)));\n float lateral = length(d.xy);\n vec2 unit = lateral > 0.0 ? d.xy / lateral : vec2(0.0);\n return uLensPrincipalPoint[i] + radius * unit;\n}\n";
3
3
  //#endregion
4
4
  export { lensModels_default as default };
@@ -1,4 +1,4 @@
1
1
  //#region ../../packages/adapters/three/src/shaders/lensSampling.glsl?raw
2
- var lensSampling_default = "// How a body direction becomes a texel of one lens, for the stitch and the seam meters: the\n// calibration as uniforms, and the sampling through each lens model. Needs lensTextures.glsl.\nuniform vec2 uFeather;\nuniform mat3 uLensRotation[MAX_LENSES];\nuniform int uLensKind[MAX_LENSES];\nuniform vec2 uLensPrincipalPoint[MAX_LENSES];\nuniform vec2 uLensFocal[MAX_LENSES];\nuniform float uLensXi[MAX_LENSES];\nuniform vec3 uLensRadial[MAX_LENSES];\nuniform vec2 uLensTangential[MAX_LENSES];\nuniform vec4 uLensPolynomial[MAX_LENSES];\nuniform float uLensHalfFov[MAX_LENSES];\nuniform vec4 uLensWindow[MAX_LENSES];\n\nstruct LensSample {\n bool isImaged;\n // Angle from the lens's optical axis.\n float theta;\n vec3 color;\n};\n\n// Where lens i images direction d on its calibration canvas; the principal point, and not\n// known, for a kind of lens this shader does not know.\nvec2 canvasPixel(int i, vec3 d, float theta, out bool isKnown) {\n isKnown = true;\n if (uLensKind[i] == LENS_MEI) {\n return projectMei(\n d,\n uLensXi[i],\n uLensFocal[i],\n uLensPrincipalPoint[i],\n uLensRadial[i],\n uLensTangential[i]\n );\n }\n if (uLensKind[i] == LENS_RADIAL_POLYNOMIAL) {\n return projectRadialPolynomial(d, theta, uLensPolynomial[i], uLensPrincipalPoint[i]);\n }\n isKnown = false;\n return uLensPrincipalPoint[i];\n}\n\n// What lens i shows in a body direction, read as `sampling` says, if it images it at all:\n// outside its field, or beyond the window the frame shows, nothing. The footprint is taken in\n// every fragment before the gates, so its derivatives are defined.\nLensSample sampleLensWith(int i, vec3 dirBody, int sampling) {\n vec3 d = uLensRotation[i] * dirBody;\n // atan keeps its precision near the axis where acos loses it.\n float theta = atan(length(d.xy), d.z);\n bool isKnown;\n vec2 pixel = canvasPixel(i, d, theta, isKnown);\n vec2 windowUv = (pixel - uLensWindow[i].xy) / uLensWindow[i].zw;\n Footprint footprint = footprintOf(i, windowUv);\n bool isInWindow =\n all(greaterThanEqual(windowUv, vec2(0.0))) && all(lessThanEqual(windowUv, vec2(1.0)));\n LensSample result;\n result.theta = theta;\n result.isImaged = isKnown && theta < uLensHalfFov[i] && isInWindow;\n result.color = result.isImaged ? sampleLens(uLensTexture[i], footprint, sampling).rgb : vec3(0.0);\n return result;\n}\n\n// What lens i shows in a body direction, read as the picture is.\nLensSample sampleLensAt(int i, vec3 dirBody) {\n return sampleLensWith(i, dirBody, uSampling);\n}\n";
2
+ var lensSampling_default = "// How a body direction becomes a texel of one lens, for the stitch and the seam meters: the\n// pose and field as uniforms, and the sampling through each lens model, the texel as its\n// exposure signal. Needs lensTextures.glsl, displayConversion.glsl and lensModels.glsl.\nuniform vec2 uFeather;\nuniform mat3 uLensRotation[MAX_LENSES];\nuniform int uLensKind[MAX_LENSES];\nuniform float uLensHalfFov[MAX_LENSES];\nuniform vec4 uLensWindow[MAX_LENSES];\n\nstruct LensSample {\n bool isImaged;\n // Angle from the lens's optical axis.\n float theta;\n // The texel where the lens's exposure is a factor (displayConversion.glsl).\n vec3 signal;\n};\n\n// Where lens i images direction d on its calibration canvas; the principal point, and not\n// known, for a kind of lens this shader does not know.\nvec2 canvasPixel(int i, vec3 d, float theta, out bool isKnown) {\n isKnown = true;\n if (uLensKind[i] == LENS_MEI) {\n return projectMei(i, d);\n }\n if (uLensKind[i] == LENS_RADIAL_POLYNOMIAL) {\n return projectRadialPolynomial(i, d, theta);\n }\n isKnown = false;\n return uLensPrincipalPoint[i];\n}\n\n// What lens i shows in a body direction, read as `sampling` says, if it images it at all:\n// outside its field, or beyond the window the frame shows, nothing. The footprint is taken in\n// every fragment before the gates, so its derivatives are defined.\nLensSample sampleLensWith(int i, vec3 dirBody, int sampling) {\n vec3 d = uLensRotation[i] * dirBody;\n // atan keeps its precision near the axis where acos loses it.\n float theta = atan(length(d.xy), d.z);\n bool isKnown;\n vec2 pixel = canvasPixel(i, d, theta, isKnown);\n vec2 windowUv = (pixel - uLensWindow[i].xy) / uLensWindow[i].zw;\n Footprint footprint = footprintOf(i, windowUv);\n bool isInWindow =\n all(greaterThanEqual(windowUv, vec2(0.0))) && all(lessThanEqual(windowUv, vec2(1.0)));\n LensSample result;\n result.theta = theta;\n result.isImaged = isKnown && theta < uLensHalfFov[i] && isInWindow;\n result.signal = result.isImaged\n ? exposureSignalOf(i, sampleLens(uLensTexture[i], footprint, sampling).rgb)\n : vec3(0.0);\n return result;\n}\n\n// What lens i shows in a body direction, read as the picture is.\nLensSample sampleLensAt(int i, vec3 dirBody) {\n return sampleLensWith(i, dirBody, uSampling);\n}\n";
3
3
  //#endregion
4
4
  export { lensSampling_default as default };