• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 23357312823

20 Mar 2026 06:35PM UTC coverage: 58.158% (+5.9%) from 52.213%
23357312823

Pull #2555

github

web-flow
Merge 71b78bbe2 into fc5791b65
Pull Request #2555: chore: Run tests on src instead of dist

3021 of 6029 branches covered (50.11%)

Branch coverage included in aggregate %.

7102 of 11377 relevant lines covered (62.42%)

243.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.65
/modules/engine/src/models/light-model-utils.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {Buffer, Device, type RenderPass, type RenderPipelineParameters} from '@luma.gl/core';
6
import {Matrix4, type NumericArray} from '@math.gl/core';
7
import type {
8
  DirectionalLight,
9
  Light,
10
  PointLight,
11
  ShaderModule,
12
  SpotLight
13
} from '@luma.gl/shadertools';
14
import {Model, type ModelProps} from '../model/model';
15
import {ShaderInputs} from '../shader-inputs';
16
import type {Geometry} from '../geometry/geometry';
17

18
const DEFAULT_POINT_LIGHT_RADIUS_FACTOR = 0.02;
58✔
19
const DEFAULT_SPOT_LIGHT_LENGTH_FACTOR = 0.12;
58✔
20
const DEFAULT_DIRECTIONAL_LIGHT_LENGTH_FACTOR = 0.15;
58✔
21
const DEFAULT_DIRECTIONAL_LIGHT_RADIUS_FACTOR = 0.2;
58✔
22
const DEFAULT_DIRECTION_FALLBACK: [number, number, number] = [0, 1, 0];
58✔
23
const DEFAULT_LIGHT_COLOR: [number, number, number] = [255, 255, 255];
58✔
24
const DEFAULT_MARKER_SCALE = 1;
58✔
25
const DIRECTIONAL_ANCHOR_DISTANCE_FACTOR = 0.35;
58✔
26
const LIGHT_COLOR_FACTOR = 255;
58✔
27
const MIN_SCENE_SCALE = 1;
58✔
28
const SPOTLIGHT_OUTER_CONE_EPSILON = 0.01;
58✔
29

30
export type LightModelBounds = [[number, number, number], [number, number, number]];
31

32
export type BaseLightModelProps = Omit<
33
  ModelProps,
34
  'geometry' | 'modules' | 'shaderInputs' | 'source' | 'vs' | 'fs' | 'instanceCount'
35
> & {
36
  lights: ReadonlyArray<Light>;
37
  viewMatrix: NumericArray;
38
  projectionMatrix: NumericArray;
39
  bounds?: LightModelBounds;
40
  markerScale?: number;
41
};
42

43
export type PointLightModelProps = BaseLightModelProps & {
44
  pointLightRadius?: number;
45
};
46

47
export type SpotLightModelProps = BaseLightModelProps & {
48
  spotLightLength?: number;
49
};
50

51
export type DirectionalLightModelProps = BaseLightModelProps & {
52
  directionalLightLength?: number;
53
};
54

55
type LightMarkerUniforms = {
56
  viewProjectionMatrix: Matrix4;
57
};
58

59
export type LightMarkerInstanceData = {
60
  instanceCount: number;
61
  instancePositions: Float32Array;
62
  instanceDirections: Float32Array;
63
  instanceScales: Float32Array;
64
  instanceColors: Float32Array;
65
};
66

67
type ManagedInstanceBuffers = Record<
68
  'instancePosition' | 'instanceDirection' | 'instanceScale' | 'instanceColor',
69
  Buffer
70
>;
71

72
type LightMarkerAnchorMode = 'centered' | 'apex';
73

74
type LightMarkerModelOptions<PropsT extends BaseLightModelProps> = {
75
  anchorMode: LightMarkerAnchorMode;
76
  buildInstanceData: (props: PropsT) => LightMarkerInstanceData;
77
  geometry: Geometry;
78
  idPrefix: string;
79
  sizePropNames: Array<keyof PropsT>;
80
};
81

82
const LIGHT_MARKER_PARAMETERS: RenderPipelineParameters = {
58✔
83
  depthCompare: 'less-equal',
84
  depthWriteEnabled: false,
85
  cullMode: 'none'
86
};
87

88
const INSTANCE_BUFFER_LAYOUT = [
58✔
89
  {name: 'instancePosition', format: 'float32x3', stepMode: 'instance'},
90
  {name: 'instanceDirection', format: 'float32x3', stepMode: 'instance'},
91
  {name: 'instanceScale', format: 'float32x3', stepMode: 'instance'},
92
  {name: 'instanceColor', format: 'float32x4', stepMode: 'instance'}
93
] as const;
94

95
const lightMarker = {
58✔
96
  name: 'lightMarker',
97
  props: {} as LightMarkerUniforms,
98
  uniforms: {} as LightMarkerUniforms,
99
  uniformTypes: {
100
    viewProjectionMatrix: 'mat4x4<f32>'
101
  }
102
} as const satisfies ShaderModule<LightMarkerUniforms, LightMarkerUniforms>;
103

104
const CENTERED_LOCAL_POSITION_WGSL = 'inputs.positions * inputs.instanceScale';
58✔
105
const APEX_LOCAL_POSITION_WGSL =
106
  'vec3<f32>(inputs.positions.x * inputs.instanceScale.x, (inputs.positions.y - 0.5) * inputs.instanceScale.y, inputs.positions.z * inputs.instanceScale.z)';
58✔
107
const CENTERED_LOCAL_POSITION_GLSL = 'positions * instanceScale';
58✔
108
const APEX_LOCAL_POSITION_GLSL =
109
  'vec3(positions.x * instanceScale.x, (positions.y - 0.5) * instanceScale.y, positions.z * instanceScale.z)';
58✔
110

111
export abstract class BaseLightModel<PropsT extends BaseLightModelProps> extends Model {
112
  protected lightModelProps: PropsT;
113
  protected _instanceData: LightMarkerInstanceData;
114
  protected _managedBuffers: ManagedInstanceBuffers;
115

116
  private readonly buildInstanceData: (props: PropsT) => LightMarkerInstanceData;
117
  private readonly sizePropNames: Array<keyof PropsT>;
118

119
  constructor(device: Device, props: PropsT, options: LightMarkerModelOptions<PropsT>) {
120
    const instanceData = options.buildInstanceData(props);
16✔
121
    const managedBuffers = createManagedInstanceBuffers(
16✔
122
      device,
123
      props.id || options.idPrefix,
32✔
124
      instanceData
125
    );
126
    const shaderInputs = new ShaderInputs<{
16✔
127
      lightMarker: typeof lightMarker.props;
128
    }>({lightMarker});
129
    shaderInputs.setProps({
16✔
130
      lightMarker: {viewProjectionMatrix: createViewProjectionMatrix(props)}
131
    });
132

133
    const {source, vs, fs} = getLightMarkerShaders(options.anchorMode);
16✔
134
    const modelProps: ModelProps = props;
16✔
135

136
    super(device, {
16✔
137
      ...modelProps,
138
      id: props.id || options.idPrefix,
32✔
139
      source,
140
      vs,
141
      fs,
142
      geometry: options.geometry,
143
      shaderInputs,
144
      bufferLayout: [...INSTANCE_BUFFER_LAYOUT],
145
      attributes: managedBuffers,
146
      instanceCount: instanceData.instanceCount,
147
      parameters: mergeLightMarkerParameters(props.parameters)
148
    });
149

150
    this.lightModelProps = props;
16✔
151
    this._instanceData = instanceData;
16✔
152
    this._managedBuffers = managedBuffers;
16✔
153
    this.buildInstanceData = options.buildInstanceData;
16✔
154
    this.sizePropNames = options.sizePropNames;
16✔
155
  }
156

157
  override destroy(): void {
158
    super.destroy();
16✔
159
    destroyManagedInstanceBuffers(this._managedBuffers);
16✔
160
    this._managedBuffers = {} as ManagedInstanceBuffers;
16✔
161
  }
162

163
  override draw(renderPass: RenderPass): boolean {
164
    if (this.instanceCount === 0) {
9✔
165
      return true;
3✔
166
    }
167
    return super.draw(renderPass);
6✔
168
  }
169

170
  setProps(props: Partial<PropsT>): void {
171
    this.lightModelProps = {...this.lightModelProps, ...props};
8✔
172

173
    if (props.parameters) {
8!
174
      this.setParameters(mergeLightMarkerParameters(this.lightModelProps.parameters));
×
175
    }
176

177
    if ('viewMatrix' in props || 'projectionMatrix' in props) {
8✔
178
      this.shaderInputs.setProps({
1✔
179
        lightMarker: {viewProjectionMatrix: createViewProjectionMatrix(this.lightModelProps)}
180
      });
181
      this.setNeedsRedraw('lightMarker camera');
1✔
182
    }
183

184
    if (shouldRebuildInstanceData(props, this.sizePropNames)) {
8✔
185
      this.rebuildInstanceData();
7✔
186
    }
187
  }
188

189
  private rebuildInstanceData(): void {
190
    const nextInstanceData = this.buildInstanceData(this.lightModelProps);
7✔
191
    const nextManagedBuffers = createManagedInstanceBuffers(this.device, this.id, nextInstanceData);
7✔
192

193
    this.setAttributes(nextManagedBuffers);
7✔
194
    this.setInstanceCount(nextInstanceData.instanceCount);
7✔
195

196
    destroyManagedInstanceBuffers(this._managedBuffers);
7✔
197
    this._managedBuffers = nextManagedBuffers;
7✔
198
    this._instanceData = nextInstanceData;
7✔
199
  }
200
}
201

202
export function buildPointLightInstanceData(props: PointLightModelProps): LightMarkerInstanceData {
203
  const pointLights = getPointLights(props.lights);
9✔
204
  const context = getLightMarkerContext(props);
9✔
205
  const pointLightRadius =
206
    props.pointLightRadius ??
9✔
207
    DEFAULT_POINT_LIGHT_RADIUS_FACTOR * context.sceneScale * context.markerScale;
208

209
  return createLightMarkerInstanceData(
9✔
210
    pointLights.length,
211
    (light, _index) => ({
9✔
212
      color: getDisplayColor(light),
213
      direction: DEFAULT_DIRECTION_FALLBACK,
214
      position: light.position,
215
      scale: [pointLightRadius, pointLightRadius, pointLightRadius]
216
    }),
217
    pointLights
218
  );
219
}
220

221
export function buildSpotLightInstanceData(props: SpotLightModelProps): LightMarkerInstanceData {
222
  const spotLights = getSpotLights(props.lights);
6✔
223
  const context = getLightMarkerContext(props);
6✔
224
  const spotLightLength =
225
    props.spotLightLength ??
6✔
226
    DEFAULT_SPOT_LIGHT_LENGTH_FACTOR * context.sceneScale * context.markerScale;
227

228
  return createLightMarkerInstanceData(
6✔
229
    spotLights.length,
230
    (light, _index) => {
231
      const outerConeAngle = clamp(
5✔
232
        light.outerConeAngle ?? Math.PI / 4,
8✔
233
        0,
234
        Math.PI / 2 - SPOTLIGHT_OUTER_CONE_EPSILON
235
      );
236
      const radius = Math.tan(outerConeAngle) * spotLightLength;
5✔
237

238
      return {
5✔
239
        color: getDisplayColor(light),
240
        direction: normalizeDirection(light.direction),
241
        position: light.position,
242
        scale: [radius, spotLightLength, radius]
243
      };
244
    },
245
    spotLights
246
  );
247
}
248

249
export function buildDirectionalLightInstanceData(
250
  props: DirectionalLightModelProps
251
): LightMarkerInstanceData {
252
  const directionalLights = getDirectionalLights(props.lights);
8✔
253
  const context = getLightMarkerContext(props);
8✔
254
  const directionalLightLength =
255
    props.directionalLightLength ??
8✔
256
    DEFAULT_DIRECTIONAL_LIGHT_LENGTH_FACTOR * context.sceneScale * context.markerScale;
257
  const directionalLightRadius = directionalLightLength * DEFAULT_DIRECTIONAL_LIGHT_RADIUS_FACTOR;
8✔
258

259
  return createLightMarkerInstanceData(
8✔
260
    directionalLights.length,
261
    (light, _index) => {
262
      const direction = normalizeDirection(light.direction);
7✔
263
      const position = [
7✔
264
        context.sceneCenter[0] -
265
          direction[0] * context.sceneScale * DIRECTIONAL_ANCHOR_DISTANCE_FACTOR,
266
        context.sceneCenter[1] -
267
          direction[1] * context.sceneScale * DIRECTIONAL_ANCHOR_DISTANCE_FACTOR,
268
        context.sceneCenter[2] -
269
          direction[2] * context.sceneScale * DIRECTIONAL_ANCHOR_DISTANCE_FACTOR
270
      ] as [number, number, number];
271

272
      return {
7✔
273
        color: getDisplayColor(light),
274
        direction,
275
        position,
276
        scale: [directionalLightRadius, directionalLightLength, directionalLightRadius]
277
      };
278
    },
279
    directionalLights
280
  );
281
}
282

283
export function getPointLights(lights: ReadonlyArray<Light>): PointLight[] {
284
  return lights.filter((light): light is PointLight => light.type === 'point');
51✔
285
}
286

287
export function getSpotLights(lights: ReadonlyArray<Light>): SpotLight[] {
288
  return lights.filter((light): light is SpotLight => light.type === 'spot');
47✔
289
}
290

291
export function getDirectionalLights(lights: ReadonlyArray<Light>): DirectionalLight[] {
292
  return lights.filter((light): light is DirectionalLight => light.type === 'directional');
14✔
293
}
294

295
export function getLightMarkerContext(props: BaseLightModelProps): {
296
  bounds: LightModelBounds;
297
  markerScale: number;
298
  sceneCenter: [number, number, number];
299
  sceneScale: number;
300
} {
301
  const bounds = getSceneBounds(props.lights, props.bounds);
23✔
302
  const sceneCenter = [
23✔
303
    (bounds[0][0] + bounds[1][0]) / 2,
304
    (bounds[0][1] + bounds[1][1]) / 2,
305
    (bounds[0][2] + bounds[1][2]) / 2
306
  ] as [number, number, number];
307
  const sceneScale = Math.max(
23✔
308
    Math.hypot(
309
      bounds[1][0] - bounds[0][0],
310
      bounds[1][1] - bounds[0][1],
311
      bounds[1][2] - bounds[0][2]
312
    ),
313
    MIN_SCENE_SCALE
314
  );
315

316
  return {
23✔
317
    bounds,
318
    markerScale: Math.max(props.markerScale ?? DEFAULT_MARKER_SCALE, 0),
46✔
319
    sceneCenter,
320
    sceneScale
321
  };
322
}
323

324
export function getDisplayColor(light: {
325
  color?: Readonly<[number, number, number]>;
326
  intensity?: number;
327
}): [number, number, number, number] {
328
  const color = light.color || DEFAULT_LIGHT_COLOR;
21!
329
  const intensity = Math.max(light.intensity ?? 1, 0);
21✔
330
  const brightness = clamp(0.35 + 0.3 * Math.log10(intensity + 1), 0.35, 1);
21✔
331

332
  return [
21✔
333
    clamp(color[0] / LIGHT_COLOR_FACTOR, 0, 1) * brightness,
334
    clamp(color[1] / LIGHT_COLOR_FACTOR, 0, 1) * brightness,
335
    clamp(color[2] / LIGHT_COLOR_FACTOR, 0, 1) * brightness,
336
    1
337
  ];
338
}
339

340
export function normalizeDirection(
341
  direction?: Readonly<[number, number, number]>
342
): [number, number, number] {
343
  const [x, y, z] = direction || DEFAULT_DIRECTION_FALLBACK;
12!
344
  const length = Math.hypot(x, y, z);
12✔
345
  if (length === 0) {
12!
346
    return [...DEFAULT_DIRECTION_FALLBACK];
×
347
  }
348
  return [x / length, y / length, z / length];
12✔
349
}
350

351
function createLightMarkerInstanceData<TLight>(
352
  instanceCount: number,
353
  getInstance: (
354
    light: TLight,
355
    index: number
356
  ) => {
357
    color: [number, number, number, number];
358
    direction: [number, number, number];
359
    position: Readonly<NumericArray>;
360
    scale: [number, number, number];
361
  },
362
  lights: TLight[] = []
23✔
363
): LightMarkerInstanceData {
364
  const instancePositions = new Float32Array(instanceCount * 3);
23✔
365
  const instanceDirections = new Float32Array(instanceCount * 3);
23✔
366
  const instanceScales = new Float32Array(instanceCount * 3);
23✔
367
  const instanceColors = new Float32Array(instanceCount * 4);
23✔
368

369
  for (const [index, light] of lights.entries()) {
23✔
370
    const instance = getInstance(light, index);
21✔
371
    instancePositions.set(instance.position, index * 3);
21✔
372
    instanceDirections.set(instance.direction, index * 3);
21✔
373
    instanceScales.set(instance.scale, index * 3);
21✔
374
    instanceColors.set(instance.color, index * 4);
21✔
375
  }
376

377
  return {
23✔
378
    instanceCount,
379
    instancePositions,
380
    instanceDirections,
381
    instanceScales,
382
    instanceColors
383
  };
384
}
385

386
function getSceneBounds(lights: ReadonlyArray<Light>, bounds?: LightModelBounds): LightModelBounds {
387
  if (bounds) {
23✔
388
    return cloneBounds(bounds);
1✔
389
  }
390

391
  const positions = [
22✔
392
    ...getPointLights(lights).map(light => light.position),
12✔
393
    ...getSpotLights(lights).map(light => light.position)
8✔
394
  ];
395

396
  if (positions.length === 0) {
22✔
397
    return [
9✔
398
      [-0.5, -0.5, -0.5],
399
      [0.5, 0.5, 0.5]
400
    ];
401
  }
402

403
  const minBounds: [number, number, number] = [...positions[0]] as [number, number, number];
13✔
404
  const maxBounds: [number, number, number] = [...positions[0]] as [number, number, number];
13✔
405

406
  for (const position of positions.slice(1)) {
13✔
407
    minBounds[0] = Math.min(minBounds[0], position[0]);
7✔
408
    minBounds[1] = Math.min(minBounds[1], position[1]);
7✔
409
    minBounds[2] = Math.min(minBounds[2], position[2]);
7✔
410

411
    maxBounds[0] = Math.max(maxBounds[0], position[0]);
7✔
412
    maxBounds[1] = Math.max(maxBounds[1], position[1]);
7✔
413
    maxBounds[2] = Math.max(maxBounds[2], position[2]);
7✔
414
  }
415

416
  return [minBounds, maxBounds];
13✔
417
}
418

419
function cloneBounds(bounds: LightModelBounds): LightModelBounds {
420
  return [[...bounds[0]] as [number, number, number], [...bounds[1]] as [number, number, number]];
1✔
421
}
422

423
function createManagedInstanceBuffers(
424
  device: Device,
425
  idPrefix: string,
426
  instanceData: LightMarkerInstanceData
427
): ManagedInstanceBuffers {
428
  return {
23✔
429
    instancePosition: device.createBuffer({
430
      id: `${idPrefix}-instance-position`,
431
      data: getBufferDataOrPlaceholder(instanceData.instancePositions, 3)
432
    }),
433
    instanceDirection: device.createBuffer({
434
      id: `${idPrefix}-instance-direction`,
435
      data: getBufferDataOrPlaceholder(instanceData.instanceDirections, 3)
436
    }),
437
    instanceScale: device.createBuffer({
438
      id: `${idPrefix}-instance-scale`,
439
      data: getBufferDataOrPlaceholder(instanceData.instanceScales, 3)
440
    }),
441
    instanceColor: device.createBuffer({
442
      id: `${idPrefix}-instance-color`,
443
      data: getBufferDataOrPlaceholder(instanceData.instanceColors, 4)
444
    })
445
  };
446
}
447

448
function getBufferDataOrPlaceholder(data: Float32Array, size: number): Float32Array {
449
  return data.length > 0 ? data : new Float32Array(size);
92✔
450
}
451

452
function destroyManagedInstanceBuffers(managedBuffers: Partial<ManagedInstanceBuffers>): void {
453
  for (const buffer of Object.values(managedBuffers)) {
23✔
454
    buffer?.destroy();
92✔
455
  }
456
}
457

458
function createViewProjectionMatrix(
459
  props: Pick<BaseLightModelProps, 'projectionMatrix' | 'viewMatrix'>
460
): Matrix4 {
461
  return new Matrix4(props.projectionMatrix).multiplyRight(props.viewMatrix);
17✔
462
}
463

464
function shouldRebuildInstanceData<PropsT extends BaseLightModelProps>(
465
  props: Partial<PropsT>,
466
  sizePropNames: Array<keyof PropsT>
467
): boolean {
468
  if ('lights' in props || 'bounds' in props || 'markerScale' in props) {
8✔
469
    return true;
7✔
470
  }
471
  return sizePropNames.some(sizePropName => sizePropName in props);
1✔
472
}
473

474
function mergeLightMarkerParameters(
475
  parameters?: RenderPipelineParameters
476
): RenderPipelineParameters {
477
  return {
16✔
478
    ...LIGHT_MARKER_PARAMETERS,
479
    ...(parameters || {})
32✔
480
  };
481
}
482

483
function getLightMarkerShaders(anchorMode: LightMarkerAnchorMode): {
484
  fs: string;
485
  source: string;
486
  vs: string;
487
} {
488
  const localPositionWGSL =
489
    anchorMode === 'apex' ? APEX_LOCAL_POSITION_WGSL : CENTERED_LOCAL_POSITION_WGSL;
16✔
490
  const localPositionGLSL =
491
    anchorMode === 'apex' ? APEX_LOCAL_POSITION_GLSL : CENTERED_LOCAL_POSITION_GLSL;
16✔
492

493
  return {
16✔
494
    source: `\
495
struct lightMarkerUniforms {
496
  viewProjectionMatrix: mat4x4<f32>,
497
};
498

499
@binding(0) @group(0) var<uniform> lightMarker : lightMarkerUniforms;
500

501
struct VertexInputs {
502
  @location(0) positions : vec3<f32>,
503
  @location(1) instancePosition : vec3<f32>,
504
  @location(2) instanceDirection : vec3<f32>,
505
  @location(3) instanceScale : vec3<f32>,
506
  @location(4) instanceColor : vec4<f32>,
507
};
508

509
struct FragmentInputs {
510
  @builtin(position) Position : vec4<f32>,
511
  @location(0) color : vec4<f32>,
512
};
513

514
fn lightMarker_rotate(localPosition: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
515
  let forward = normalize(direction);
516
  var helperAxis = vec3<f32>(0.0, 1.0, 0.0);
517
  if (abs(forward.y) > 0.999) {
518
    helperAxis = vec3<f32>(1.0, 0.0, 0.0);
519
  }
520

521
  let tangent = normalize(cross(helperAxis, forward));
522
  let bitangent = cross(forward, tangent);
523
  return tangent * localPosition.x + forward * localPosition.y + bitangent * localPosition.z;
524
}
525

526
@vertex
527
fn vertexMain(inputs: VertexInputs) -> FragmentInputs {
528
  var outputs : FragmentInputs;
529
  let localPosition = ${localPositionWGSL};
530
  let worldPosition = inputs.instancePosition + lightMarker_rotate(localPosition, inputs.instanceDirection);
531
  outputs.Position = lightMarker.viewProjectionMatrix * vec4<f32>(worldPosition, 1.0);
532
  outputs.color = inputs.instanceColor;
533
  return outputs;
534
}
535

536
@fragment
537
fn fragmentMain(inputs: FragmentInputs) -> @location(0) vec4<f32> {
538
  return inputs.color;
539
}
540
`,
541
    vs: `\
542
#version 300 es
543

544
in vec3 positions;
545
in vec3 instancePosition;
546
in vec3 instanceDirection;
547
in vec3 instanceScale;
548
in vec4 instanceColor;
549

550
uniform lightMarkerUniforms {
551
  mat4 viewProjectionMatrix;
552
} lightMarker;
553

554
out vec4 vColor;
555

556
vec3 lightMarker_rotate(vec3 localPosition, vec3 direction) {
557
  vec3 forward = normalize(direction);
558
  vec3 helperAxis = abs(forward.y) > 0.999 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0);
559
  vec3 tangent = normalize(cross(helperAxis, forward));
560
  vec3 bitangent = cross(forward, tangent);
561
  return tangent * localPosition.x + forward * localPosition.y + bitangent * localPosition.z;
562
}
563

564
void main(void) {
565
  vec3 localPosition = ${localPositionGLSL};
566
  vec3 worldPosition = instancePosition + lightMarker_rotate(localPosition, instanceDirection);
567
  gl_Position = lightMarker.viewProjectionMatrix * vec4(worldPosition, 1.0);
568
  vColor = instanceColor;
569
}
570
`,
571
    fs: `\
572
#version 300 es
573
precision highp float;
574

575
in vec4 vColor;
576
out vec4 fragColor;
577

578
void main(void) {
579
  fragColor = vColor;
580
}
581
`
582
  };
583
}
584

585
function clamp(value: number, minValue: number, maxValue: number): number {
586
  return Math.min(maxValue, Math.max(minValue, value));
89✔
587
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc