three-ilda 0.1.0
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/CREDITS.md +6 -0
- package/LICENSE +21 -0
- package/README.md +354 -0
- package/package.json +93 -0
- package/src/core/LaserScanner.js +97 -0
- package/src/core/LaserShowBase.js +224 -0
- package/src/index.js +10 -0
- package/src/loaders/ILDALoader.js +160 -0
- package/src/objects/LaserShow.js +25 -0
- package/src/postprocessing/LaserShowPipeline.js +119 -0
- package/src/webgl/LaserShow.js +85 -0
- package/src/webgl/LaserShowPipeline.js +168 -0
- package/src/webgl.js +10 -0
- package/types/core/LaserScanner.d.ts +85 -0
- package/types/core/LaserShowBase.d.ts +166 -0
- package/types/index.d.ts +10 -0
- package/types/loaders/ILDALoader.d.ts +124 -0
- package/types/objects/LaserShow.d.ts +19 -0
- package/types/postprocessing/LaserShowPipeline.d.ts +89 -0
- package/types/webgl/LaserShow.d.ts +28 -0
- package/types/webgl/LaserShowPipeline.d.ts +99 -0
- package/types/webgl.d.ts +10 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { BufferAttribute, BufferGeometry, DynamicDrawUsage, Group, LineSegments, Mesh, Object3D, Vector2 } from 'three'
|
|
2
|
+
import { LaserScanner, SCANNER_DEFAULTS, TRAIL_POINTS } from './LaserScanner.js'
|
|
3
|
+
|
|
4
|
+
function number(value, min, max, name) {
|
|
5
|
+
if (!Number.isFinite(value) || value < min || value > max) throw new RangeError(`${name} must be between ${min} and ${max}.`)
|
|
6
|
+
return value
|
|
7
|
+
}
|
|
8
|
+
function dynamic(size, width = 3) { return new BufferAttribute(new Float32Array(size), width).setUsage(DynamicDrawUsage) }
|
|
9
|
+
function upload(attribute, size) {
|
|
10
|
+
attribute.clearUpdateRanges()
|
|
11
|
+
if (size) { attribute.addUpdateRange(0, size); attribute.needsUpdate = true }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @typedef {import('../loaders/ILDALoader.js').ILDAAsset} ILDAAsset */
|
|
15
|
+
/** @typedef {'Disabled' | 'Front' | 'Back'} BeamMode */
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} LaserShowOptions
|
|
18
|
+
* @property {number} [track] Projector track to play; defaults to the first track of the asset.
|
|
19
|
+
* @property {number} [capacity] Trail length in samples, 2 to 65536 (default 2048).
|
|
20
|
+
* @property {number} [width] Projection width in local units at zoom 1 (default 4.8).
|
|
21
|
+
* @property {number} [height] Projection height in local units at zoom 1 (default 3.2).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Renderer-independent part of a laser show: scene graph, scanner playback, segment compaction and buffer uploads.
|
|
26
|
+
* The image lies in local XY at Z=0; Front is +Z, Back is -Z, regardless of the camera.
|
|
27
|
+
* One instance plays one ILDA projector track. Assets may be shared; playback/GPU buffers are independent.
|
|
28
|
+
* A subclass supplies the two materials through _createMaterials(); the uniform handles it returns only need a `.value`.
|
|
29
|
+
*/
|
|
30
|
+
export class LaserShowBase extends Group {
|
|
31
|
+
/**
|
|
32
|
+
* @param {ILDAAsset | null} [asset] Parsed ILDA file; call setData() later when omitted.
|
|
33
|
+
* @param {LaserShowOptions} [options]
|
|
34
|
+
*/
|
|
35
|
+
constructor(asset = null, { track, capacity = TRAIL_POINTS, width = 4.8, height = 3.2 } = {}) {
|
|
36
|
+
super()
|
|
37
|
+
if (!Number.isInteger(capacity) || capacity < 2 || capacity > 65536) throw new RangeError('capacity must be an integer from 2 to 65536.')
|
|
38
|
+
this.type = 'LaserShow'; this.name = 'ILDA laser show'; this.isLaserShow = true
|
|
39
|
+
this.capacity = capacity; this.playing = true; this.disposed = false
|
|
40
|
+
/** @type {import('./LaserScanner.js').ScannerParams} */
|
|
41
|
+
this._params = { ...SCANNER_DEFAULTS }
|
|
42
|
+
/** @type {BeamMode} */
|
|
43
|
+
this._beamMode = 'Disabled'
|
|
44
|
+
this._zoom = 1; this._dirty = true
|
|
45
|
+
/** @type {ILDAAsset | null} */ this.asset = null
|
|
46
|
+
/** @type {number | undefined} */ this.track = undefined
|
|
47
|
+
/** @type {LaserScanner | null} */ this.scanner = null
|
|
48
|
+
this._projectionSize = new Vector2(width, height)
|
|
49
|
+
const { line, beam, scale, origin, clock, intensity, beamIntensity } = this._createMaterials()
|
|
50
|
+
this._scale = scale; this._origin = origin; this._clock = clock; this._intensity = intensity; this._beamIntensity = beamIntensity
|
|
51
|
+
this.projector = new Object3D(); this.projector.name = 'Projector aperture'; this.projector.position.set(-2.2, -1.4, 3)
|
|
52
|
+
this.add(this.projector)
|
|
53
|
+
|
|
54
|
+
const segments = capacity - 1
|
|
55
|
+
this._linePosition = dynamic(segments * 6); this._lineColor = dynamic(segments * 6)
|
|
56
|
+
const lineGeometry = new BufferGeometry()
|
|
57
|
+
lineGeometry.setAttribute('position', this._linePosition); lineGeometry.setAttribute('color', this._lineColor); lineGeometry.setDrawRange(0, 0)
|
|
58
|
+
this.projection = new LineSegments(lineGeometry, line)
|
|
59
|
+
this.projection.name = 'Laser projection'; this.projection.frustumCulled = false
|
|
60
|
+
this.add(this.projection)
|
|
61
|
+
|
|
62
|
+
this._beamPosition = dynamic(segments * 9); this._beamColor = dynamic(segments * 9)
|
|
63
|
+
const along = new Float32Array(segments * 3)
|
|
64
|
+
for (let i = 0; i < along.length; i += 3) { along[i + 1] = 1; along[i + 2] = 1 }
|
|
65
|
+
const beamGeometry = new BufferGeometry()
|
|
66
|
+
beamGeometry.setAttribute('position', this._beamPosition); beamGeometry.setAttribute('color', this._beamColor)
|
|
67
|
+
beamGeometry.setAttribute('beamAlong', new BufferAttribute(along, 1)); beamGeometry.setDrawRange(0, 0)
|
|
68
|
+
this.beams = new Mesh(beamGeometry, beam); this.beams.name = 'Soft projector beams'
|
|
69
|
+
this.beams.frustumCulled = false; this.beams.visible = false; this.add(this.beams)
|
|
70
|
+
this.setProjectionSize(width, height)
|
|
71
|
+
if (asset) this.setData(asset, track)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Creates the line and beam materials for one renderer family.
|
|
76
|
+
* @returns {{ line: import('three').Material, beam: import('three').Material, scale: { value: import('three').Vector3 }, origin: { value: import('three').Vector3 }, clock: { value: number }, intensity: { value: number }, beamIntensity: { value: number } }}
|
|
77
|
+
*/
|
|
78
|
+
_createMaterials() { throw new Error('LaserShowBase is abstract: use the LaserShow of the three-ilda or three-ilda/webgl entry.') }
|
|
79
|
+
|
|
80
|
+
get pointRate() { return this._params.rate }
|
|
81
|
+
set pointRate(v) { this._params.rate = number(v, 1, 100000, 'pointRate') }
|
|
82
|
+
get gain() { return this._params.gain }
|
|
83
|
+
set gain(v) { this._params.gain = number(v, 0.001, 2, 'gain'); this._retune() }
|
|
84
|
+
get dampening() { return this._params.dampening }
|
|
85
|
+
set dampening(v) { this._params.dampening = number(v, 0, 1, 'dampening'); this._retune() }
|
|
86
|
+
get blankingOffset() { return this._params.blankingOffset }
|
|
87
|
+
set blankingOffset(v) { this._params.blankingOffset = Math.round(number(v, -100, 100, 'blankingOffset')); this._retune() }
|
|
88
|
+
get lightDecay() { return this._params.lightDecay }
|
|
89
|
+
set lightDecay(v) { this._params.lightDecay = number(v, 0, 1, 'lightDecay'); this._dirty = true }
|
|
90
|
+
get zoom() { return this._zoom }
|
|
91
|
+
set zoom(v) { this._zoom = number(v, 0, 4, 'zoom'); this._updateScale() }
|
|
92
|
+
get intensity() { return this._intensity.value }
|
|
93
|
+
set intensity(v) { this._intensity.value = number(v, 0, 100, 'intensity') }
|
|
94
|
+
get beamIntensity() { return this._beamIntensity.value }
|
|
95
|
+
set beamIntensity(v) { this._beamIntensity.value = number(v, 0, 2, 'beamIntensity') }
|
|
96
|
+
get beamMode() { return this._beamMode }
|
|
97
|
+
/** @param {BeamMode} mode */
|
|
98
|
+
set beamMode(mode) {
|
|
99
|
+
if (!['Disabled', 'Front', 'Back'].includes(mode)) throw new RangeError('beamMode must be Disabled, Front or Back.')
|
|
100
|
+
this._beamMode = mode; this.beams.visible = mode !== 'Disabled'
|
|
101
|
+
if (mode !== 'Disabled') this.projector.position.z = Math.max(0.1, Math.abs(this.projector.position.z)) * (mode === 'Front' ? 1 : -1)
|
|
102
|
+
this._dirty = true
|
|
103
|
+
}
|
|
104
|
+
get frame() { return this.scanner?.lastFrame ?? 0 }
|
|
105
|
+
get frameCount() { return this.scanner?.frames.length ?? 0 }
|
|
106
|
+
get time() { return this.scanner?.time ?? 0 }
|
|
107
|
+
get trackIds() { return this.asset ? [...this.asset.tracks.keys()] : [] }
|
|
108
|
+
get projectionSize() { return this._projectionSize.clone() }
|
|
109
|
+
|
|
110
|
+
/** @param {number} width @param {number} height @returns {this} */
|
|
111
|
+
setProjectionSize(width, height) {
|
|
112
|
+
this._projectionSize.set(number(width, 0.001, 10000, 'width'), number(height, 0.001, 10000, 'height'))
|
|
113
|
+
this._updateScale(); return this
|
|
114
|
+
}
|
|
115
|
+
_updateScale() { this._scale.value.set(this._projectionSize.x * this.zoom / 2, this._projectionSize.y * this.zoom / 2, 0) }
|
|
116
|
+
_retune() {
|
|
117
|
+
if (this.scanner && !this.playing) this.scanner.seekFrame(this.frame, this._params)
|
|
118
|
+
this._dirty = true
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Replaces the played file, building a fresh scanner for one projector track.
|
|
122
|
+
* @param {ILDAAsset} asset
|
|
123
|
+
* @param {number} [track] Defaults to the first track of the asset.
|
|
124
|
+
* @returns {this}
|
|
125
|
+
*/
|
|
126
|
+
setData(asset, track = asset.tracks.keys().next().value) {
|
|
127
|
+
if (this.disposed) throw new Error('LaserShow has been disposed.')
|
|
128
|
+
const frames = asset?.tracks?.get(track)
|
|
129
|
+
if (!frames?.length) throw new Error(`ILDA track ${track} does not exist.`)
|
|
130
|
+
const scanner = new LaserScanner(frames, this.capacity)
|
|
131
|
+
this.asset = asset; this.track = track; this.scanner = scanner
|
|
132
|
+
this.scanner.seekFrame(0, this._params); this._dirty = true; this.update(0)
|
|
133
|
+
return this
|
|
134
|
+
}
|
|
135
|
+
/** @param {number} track @returns {this} */
|
|
136
|
+
setTrack(track) { return this.setData(this.asset, track) }
|
|
137
|
+
/** @param {number} frame @returns {this} */
|
|
138
|
+
seekFrame(frame) {
|
|
139
|
+
if (!this.scanner) return this
|
|
140
|
+
number(frame, 0, this.frameCount - 1, 'frame')
|
|
141
|
+
this.scanner.seekFrame(Math.floor(frame), this._params); this._dirty = true; this.update(0)
|
|
142
|
+
return this
|
|
143
|
+
}
|
|
144
|
+
reset() { if (this.scanner) { this.scanner.reset(); this._dirty = true; this.update(0) } return this }
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Advances playback and uploads the trail. Call once per application frame, including while paused to apply parameter changes.
|
|
148
|
+
* @param {number} [delta] Seconds since the previous call.
|
|
149
|
+
* @returns {this}
|
|
150
|
+
*/
|
|
151
|
+
update(delta = 0) {
|
|
152
|
+
if (this.disposed || !this.scanner) return this
|
|
153
|
+
if (!Number.isFinite(delta) || delta < 0) throw new RangeError('delta must be a finite non-negative number.')
|
|
154
|
+
if (this.playing && delta > 0) { this.scanner.advance(delta, this._params); this._dirty = true }
|
|
155
|
+
this._origin.value.copy(this.projector.position); this._clock.value = this.time
|
|
156
|
+
if (!this._dirty) return this
|
|
157
|
+
const out = this.scanner.snapshot(this.lightDecay)
|
|
158
|
+
const lp = this._linePosition.array, lc = this._lineColor.array, bp = this._beamPosition.array, bc = this._beamColor.array
|
|
159
|
+
let lineSize = 0, beamSize = 0
|
|
160
|
+
for (let i = 1; i < out.count; i++) {
|
|
161
|
+
if (out.blank[i - 1] || out.blank[i]) continue
|
|
162
|
+
const a = (i - 1) * 3, b = i * 3
|
|
163
|
+
if (Math.max(out.color[a], out.color[a + 1], out.color[a + 2], out.color[b], out.color[b + 1], out.color[b + 2]) < 0.001) continue
|
|
164
|
+
for (let j = 0; j < 3; j++) {
|
|
165
|
+
lp[lineSize + j] = out.position[a + j]; lp[lineSize + 3 + j] = out.position[b + j]
|
|
166
|
+
lc[lineSize + j] = out.color[a + j]; lc[lineSize + 3 + j] = out.color[b + j]
|
|
167
|
+
}
|
|
168
|
+
lineSize += 6
|
|
169
|
+
if (!this.beams.visible) continue
|
|
170
|
+
for (let j = 0; j < 3; j++) {
|
|
171
|
+
bp[beamSize + j] = 0; bp[beamSize + 3 + j] = out.position[a + j]; bp[beamSize + 6 + j] = out.position[b + j]
|
|
172
|
+
const color = (out.color[a + j] + out.color[b + j]) * 0.5
|
|
173
|
+
bc[beamSize + j] = color; bc[beamSize + 3 + j] = color; bc[beamSize + 6 + j] = color
|
|
174
|
+
}
|
|
175
|
+
beamSize += 9
|
|
176
|
+
}
|
|
177
|
+
this.projection.geometry.setDrawRange(0, lineSize / 3)
|
|
178
|
+
upload(this._linePosition, lineSize); upload(this._lineColor, lineSize)
|
|
179
|
+
if (this.beams.visible) {
|
|
180
|
+
this.beams.geometry.setDrawRange(0, beamSize / 3)
|
|
181
|
+
upload(this._beamPosition, beamSize); upload(this._beamColor, beamSize)
|
|
182
|
+
}
|
|
183
|
+
this._dirty = false; return this
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Clone shares the immutable asset, but copies scanner history and allocates independent GPU resources.
|
|
188
|
+
* @param {boolean} [recursive]
|
|
189
|
+
* @returns {this}
|
|
190
|
+
*/
|
|
191
|
+
clone(recursive = true) {
|
|
192
|
+
return new this.constructor(null, { capacity: this.capacity, width: this._projectionSize.x, height: this._projectionSize.y }).copy(this, recursive)
|
|
193
|
+
}
|
|
194
|
+
/** @param {LaserShowBase} source @param {boolean} [recursive] @returns {this} */
|
|
195
|
+
copy(source, recursive = true) {
|
|
196
|
+
if (this.disposed || source.disposed) throw new Error('Cannot copy a disposed LaserShow.')
|
|
197
|
+
if (this.capacity !== source.capacity) throw new Error('copy() requires matching trail capacities; use clone() to preserve capacity.')
|
|
198
|
+
super.copy(source, false)
|
|
199
|
+
this._params = { ...source._params }; this.playing = source.playing
|
|
200
|
+
this.setProjectionSize(source._projectionSize.x, source._projectionSize.y)
|
|
201
|
+
this.zoom = source.zoom; this.intensity = source.intensity; this.beamIntensity = source.beamIntensity; this.beamMode = source.beamMode
|
|
202
|
+
this.projector.position.copy(source.projector.position)
|
|
203
|
+
this.projection.visible = source.projection.visible; this.beams.visible = source.beams.visible
|
|
204
|
+
if (source.asset) {
|
|
205
|
+
this.setData(source.asset, source.track)
|
|
206
|
+
for (const key of ['frame', 'index', 'lastFrame', 'time', 'fraction', 'write', 'count', 'samples', 'x', 'y', 'vx', 'vy']) this.scanner[key] = source.scanner[key]
|
|
207
|
+
for (const key of ['position', 'color', 'blank', 'born']) this.scanner[key].set(source.scanner[key])
|
|
208
|
+
} else {
|
|
209
|
+
this.asset = null; this.scanner = null; this.track = undefined
|
|
210
|
+
this.projection.geometry.setDrawRange(0, 0); this.beams.geometry.setDrawRange(0, 0)
|
|
211
|
+
}
|
|
212
|
+
for (const child of [...this.children]) if (![this.projector, this.projection, this.beams].includes(child)) this.remove(child)
|
|
213
|
+
if (recursive) for (const child of source.children) if (![source.projector, source.projection, source.beams].includes(child)) this.add(child.clone())
|
|
214
|
+
this._dirty = true; this.update(0); return this
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Releases only this instance's GPU resources; shared ILDA data stays usable. */
|
|
218
|
+
dispose() {
|
|
219
|
+
if (this.disposed) return
|
|
220
|
+
this.projection.geometry.dispose(); this.projection.material.dispose()
|
|
221
|
+
this.beams.geometry.dispose(); this.beams.material.dispose()
|
|
222
|
+
this.disposed = true; this.dispatchEvent({ type: 'dispose' })
|
|
223
|
+
}
|
|
224
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** @typedef {import('./loaders/ILDALoader.js').ILDAAsset} ILDAAsset */
|
|
2
|
+
/** @typedef {import('./loaders/ILDALoader.js').ILDAFrame} ILDAFrame */
|
|
3
|
+
/** @typedef {import('./core/LaserScanner.js').ScannerParams} ScannerParams */
|
|
4
|
+
/** @typedef {import('./core/LaserShowBase.js').LaserShowOptions} LaserShowOptions */
|
|
5
|
+
/** @typedef {import('./core/LaserShowBase.js').BeamMode} BeamMode */
|
|
6
|
+
/** @typedef {import('./postprocessing/LaserShowPipeline.js').LaserShowPipelineOptions} LaserShowPipelineOptions */
|
|
7
|
+
export { ILDALoader, ILDA_DEFAULT_PALETTE } from './loaders/ILDALoader.js'
|
|
8
|
+
export { LaserScanner, SCANNER_DEFAULTS, TRAIL_POINTS } from './core/LaserScanner.js'
|
|
9
|
+
export { LaserShow } from './objects/LaserShow.js'
|
|
10
|
+
export { LaserShowPipeline } from './postprocessing/LaserShowPipeline.js'
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { FileLoader, Loader } from 'three'
|
|
2
|
+
|
|
3
|
+
const MAX_FILE_BYTES = 32 * 1024 * 1024, MAX_POINTS = 2_000_000
|
|
4
|
+
const FORMAT_SIZE = { 0: 8, 1: 6, 2: 3, 4: 10, 5: 8 }
|
|
5
|
+
const HEADER = 32
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} ILDAFrame
|
|
9
|
+
* @property {number} number Frame number from the file.
|
|
10
|
+
* @property {string} name Frame name from the file.
|
|
11
|
+
* @property {number} projector Projector ID from the file (zero-based).
|
|
12
|
+
* @property {number} count Number of points.
|
|
13
|
+
* @property {Float32Array} position Interleaved XYZ, normalized by 32768. Z is zero for 2D formats.
|
|
14
|
+
* @property {Float32Array} color Interleaved RGB in [0, 1], including colors of blanked points.
|
|
15
|
+
* @property {Uint8Array} blank One flag per point: 1 means laser off.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} ILDAAsset
|
|
20
|
+
* @property {ILDAFrame[]} frames Point frames in file order; palette records are not frames.
|
|
21
|
+
* @property {Map<number, ILDAFrame[]>} tracks Frames grouped by projector ID, sharing objects with frames.
|
|
22
|
+
* @property {number} totalPoints Total number of points in all tracks.
|
|
23
|
+
* @property {number} blanked Number of blanked points.
|
|
24
|
+
* @property {number} paletteFallbacks Number of unknown palette indices rendered in white.
|
|
25
|
+
* @property {boolean} eof Whether an explicit end-of-file record was found.
|
|
26
|
+
* @property {number} bytes Input size in bytes.
|
|
27
|
+
* @property {number} parseTimeMs CPU parsing time; excludes downloading and local file reading.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ILDA IDTF loader for formats 0, 1, 2, 4 and 5. No renderer, DOM or player dependency.
|
|
32
|
+
* Uses the standard Three.js LoadingManager, FileLoader and inherited loadAsync().
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const loader = new ILDALoader(manager);
|
|
36
|
+
* const show = await loader.loadAsync('/shows/example.ild');
|
|
37
|
+
* const localShow = loader.parse(await file.arrayBuffer());
|
|
38
|
+
*
|
|
39
|
+
* @extends Loader
|
|
40
|
+
*/
|
|
41
|
+
export class ILDALoader extends Loader {
|
|
42
|
+
/** @param {import('three').LoadingManager} [manager] */
|
|
43
|
+
constructor(manager) {
|
|
44
|
+
super(manager)
|
|
45
|
+
this.fallbackPalette = ILDA_DEFAULT_PALETTE
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Sets colors for indexed files without a format-2 palette. Embedded palettes always win.
|
|
50
|
+
* Copies the array; pass null to restore the standard ILDA palette.
|
|
51
|
+
* @param {ReadonlyArray<ReadonlyArray<number>> | null} palette Between 1 and 256 RGB triplets of integer bytes.
|
|
52
|
+
* @returns {ILDALoader}
|
|
53
|
+
*/
|
|
54
|
+
setFallbackPalette(palette) {
|
|
55
|
+
if (palette === null) { this.fallbackPalette = ILDA_DEFAULT_PALETTE; return this }
|
|
56
|
+
if (!Array.isArray(palette) || !palette.length || palette.length > 256 || Array.from(palette).some(rgb =>
|
|
57
|
+
!Array.isArray(rgb) || rgb.length !== 3 || Array.from(rgb).some(v => !Number.isInteger(v) || v < 0 || v > 255))) {
|
|
58
|
+
throw new TypeError('Expected 1–256 RGB triplets with integer values from 0 to 255.')
|
|
59
|
+
}
|
|
60
|
+
this.fallbackPalette = Object.freeze(palette.map(rgb => Object.freeze([...rgb])))
|
|
61
|
+
return this
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} url URL or path, resolved by the loading manager.
|
|
66
|
+
* @param {(asset: ILDAAsset) => void} onLoad
|
|
67
|
+
* @param {(event: ProgressEvent) => void} [onProgress]
|
|
68
|
+
* @param {(error: Error) => void} [onError] Receives download and parsing errors.
|
|
69
|
+
*/
|
|
70
|
+
load(url, onLoad, onProgress, onError) {
|
|
71
|
+
const loader = new FileLoader(this.manager)
|
|
72
|
+
loader.setPath(this.path)
|
|
73
|
+
loader.setResponseType('arraybuffer')
|
|
74
|
+
loader.setRequestHeader(this.requestHeader)
|
|
75
|
+
loader.setWithCredentials(this.withCredentials)
|
|
76
|
+
loader.load(url, buffer => {
|
|
77
|
+
try {
|
|
78
|
+
const asset = this.parse(buffer)
|
|
79
|
+
if (onLoad) onLoad(asset)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (onError) onError(error)
|
|
82
|
+
else console.error(error)
|
|
83
|
+
this.manager.itemError(url)
|
|
84
|
+
}
|
|
85
|
+
}, onProgress, onError)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parses already-loaded bytes synchronously. Does not create geometry or apply scanner settings.
|
|
90
|
+
* @param {ArrayBuffer} buffer
|
|
91
|
+
* @returns {ILDAAsset}
|
|
92
|
+
* @throws {Error} On invalid data, unsupported formats or the 32 MiB / 2M point limits.
|
|
93
|
+
*/
|
|
94
|
+
parse(buffer) {
|
|
95
|
+
const start = performance.now()
|
|
96
|
+
const asset = parseILDA(buffer, this.fallbackPalette)
|
|
97
|
+
asset.parseTimeMs = performance.now() - start
|
|
98
|
+
return asset
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ILDA IDTF revision 011, appendix A. A file's format-2 palette overrides this per projector.
|
|
103
|
+
function defaultPalette() {
|
|
104
|
+
const p = []
|
|
105
|
+
for (let i = 0; i < 16; i++) p.push([255, i * 16, 0])
|
|
106
|
+
p.push([255, 255, 0]); for (let i = 7; i > 0; i--) p.push([i * 32, 255, 0])
|
|
107
|
+
p.push([0, 255, 0]); for (const v of [36, 73, 109, 146, 182, 219, 255]) p.push([0, 255, v])
|
|
108
|
+
for (const v of [227, 198, 170, 142, 113, 85, 56, 28, 0]) p.push([0, v, 255])
|
|
109
|
+
for (const v of [32, 64, 96, 128, 160, 192, 224, 255]) p.push([v, 0, 255])
|
|
110
|
+
for (const v of [32, 64, 96, 128, 160, 192, 224, 255]) p.push([255, v, 255])
|
|
111
|
+
for (const v of [224, 192, 160, 128, 96, 64, 32]) p.push([255, v, v])
|
|
112
|
+
return p
|
|
113
|
+
}
|
|
114
|
+
export const ILDA_DEFAULT_PALETTE = Object.freeze(defaultPalette().map(rgb => Object.freeze(rgb)))
|
|
115
|
+
|
|
116
|
+
/** One DataView and a cursor: no repeated copies of the remaining file. Returns all projector tracks. */
|
|
117
|
+
function parseILDA(buffer, fallbackPalette) {
|
|
118
|
+
if (!(buffer instanceof ArrayBuffer)) throw new Error('Expected a binary ILDA file.')
|
|
119
|
+
if (buffer.byteLength > MAX_FILE_BYTES) throw new Error('ILDALoader supports files up to 32 MiB.')
|
|
120
|
+
const v = new DataView(buffer), text = new TextDecoder('ascii'), palettes = new Map(), tracks = new Map(), frames = []
|
|
121
|
+
let offset = 0, totalPoints = 0, blanked = 0, paletteFallbacks = 0, eof = false
|
|
122
|
+
while (offset < v.byteLength) {
|
|
123
|
+
if (offset + HEADER > v.byteLength) throw new Error(`Incomplete ILDA header at byte ${offset}.`)
|
|
124
|
+
if (v.getUint32(offset) !== 0x494c4441) throw new Error(`Invalid ILDA signature at byte ${offset}.`)
|
|
125
|
+
const format = v.getUint8(offset + 7), count = v.getUint16(offset + 24), projector = v.getUint8(offset + 30)
|
|
126
|
+
const size = FORMAT_SIZE[format]
|
|
127
|
+
if (!size) throw new Error(`Unsupported ILDA format ${format}. Supported formats: 0, 1, 2, 4 and 5.`)
|
|
128
|
+
const number = v.getUint16(offset + 26), name = text.decode(new Uint8Array(buffer, offset + 8, 8)).replace(/\0/g, '').trim()
|
|
129
|
+
offset += HEADER
|
|
130
|
+
if (format !== 2 && count === 0) { eof = true; break }
|
|
131
|
+
if (offset + count * size > v.byteLength) throw new Error('The ILDA file ends in the middle of a frame.')
|
|
132
|
+
if (format === 2) {
|
|
133
|
+
if (count > 256 || count === 0) throw new Error('Invalid ILDA palette size.')
|
|
134
|
+
const palette = []
|
|
135
|
+
for (let i = 0; i < count; i++, offset += 3) palette.push([v.getUint8(offset), v.getUint8(offset + 1), v.getUint8(offset + 2)])
|
|
136
|
+
palettes.set(projector, palette); continue
|
|
137
|
+
}
|
|
138
|
+
totalPoints += count
|
|
139
|
+
if (totalPoints > MAX_POINTS) throw new Error('ILDALoader supports up to 2,000,000 points.')
|
|
140
|
+
const position = new Float32Array(count * 3), color = new Float32Array(count * 3), blank = new Uint8Array(count)
|
|
141
|
+
const palette = palettes.get(projector) || fallbackPalette, is3D = format === 0 || format === 4, indexed = format === 0 || format === 1
|
|
142
|
+
for (let i = 0; i < count; i++, offset += size) {
|
|
143
|
+
const j = i * 3, statusAt = offset + (is3D ? 6 : 4)
|
|
144
|
+
position[j] = v.getInt16(offset) / 32768; position[j + 1] = v.getInt16(offset + 2) / 32768
|
|
145
|
+
position[j + 2] = is3D ? v.getInt16(offset + 4) / 32768 : 0
|
|
146
|
+
blank[i] = (v.getUint8(statusAt) & 0x40) !== 0 ? 1 : 0; blanked += blank[i]
|
|
147
|
+
if (indexed) {
|
|
148
|
+
const rgb = palette[v.getUint8(statusAt + 1)]
|
|
149
|
+
if (!rgb) paletteFallbacks++
|
|
150
|
+
color[j] = (rgb?.[0] ?? 255) / 255; color[j + 1] = (rgb?.[1] ?? 255) / 255; color[j + 2] = (rgb?.[2] ?? 255) / 255
|
|
151
|
+
} else { // true-color point records store B, G, R; palette records store R, G, B
|
|
152
|
+
color[j] = v.getUint8(statusAt + 3) / 255; color[j + 1] = v.getUint8(statusAt + 2) / 255; color[j + 2] = v.getUint8(statusAt + 1) / 255
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const frame = { number, name, projector, count, position, color, blank }
|
|
156
|
+
frames.push(frame); if (!tracks.has(projector)) tracks.set(projector, []); tracks.get(projector).push(frame)
|
|
157
|
+
}
|
|
158
|
+
if (!frames.length) throw new Error('The file contains no point frames.')
|
|
159
|
+
return { frames, tracks, totalPoints, blanked, paletteFallbacks, eof, bytes: buffer.byteLength }
|
|
160
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AdditiveBlending, DoubleSide, LineBasicNodeMaterial, MeshBasicNodeMaterial, Vector3 } from 'three/webgpu'
|
|
2
|
+
import { attribute, float, mix, mx_noise_float, positionWorld, smoothstep, uniform, varying, vec3 } from 'three/tsl'
|
|
3
|
+
import { LaserShowBase } from '../core/LaserShowBase.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Laser show for THREE.WebGPURenderer (the WebGPU / TSL version): node materials with the projection and beam math in TSL.
|
|
7
|
+
* Playback, buffers and the public API live in LaserShowBase; ../webgl/LaserShow.js is the WebGL 2 version for WebGLRenderer.
|
|
8
|
+
*/
|
|
9
|
+
export class LaserShow extends LaserShowBase {
|
|
10
|
+
_createMaterials() {
|
|
11
|
+
const scale = uniform(new Vector3()), origin = uniform(new Vector3()), clock = uniform(0), intensity = uniform(1), beamIntensity = uniform(0.5)
|
|
12
|
+
const line = new LineBasicNodeMaterial({ transparent: true, depthWrite: false, blending: AdditiveBlending })
|
|
13
|
+
line.positionNode = attribute('position', 'vec3').mul(scale)
|
|
14
|
+
line.colorNode = attribute('color', 'vec3').mul(intensity)
|
|
15
|
+
const beam = new MeshBasicNodeMaterial({ transparent: true, depthWrite: false, blending: AdditiveBlending, side: DoubleSide })
|
|
16
|
+
beam.forceSinglePass = true
|
|
17
|
+
beam.positionNode = mix(origin, attribute('position', 'vec3').mul(scale), attribute('beamAlong', 'float'))
|
|
18
|
+
const along = varying(attribute('beamAlong', 'float'))
|
|
19
|
+
const drift = positionWorld.mul(1.4).add(vec3(clock.mul(0.08), clock.mul(-0.12), 0))
|
|
20
|
+
const haze = mx_noise_float(drift).mul(0.3).add(0.7)
|
|
21
|
+
const envelope = smoothstep(0, 0.025, along).mul(float(1).sub(smoothstep(0.78, 1, along)))
|
|
22
|
+
beam.colorNode = attribute('color', 'vec3').mul(haze).mul(envelope).mul(beamIntensity)
|
|
23
|
+
return { line, beam, scale, origin, clock, intensity, beamIntensity }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Color, Layers, PassNode, RenderPipeline } from 'three/webgpu'
|
|
2
|
+
import { float, max, orthographicDepthToViewZ, pass, perspectiveDepthToViewZ, positionView, screenUV, smoothstep, uniform, vec2, vec3, vec4 } from 'three/tsl'
|
|
3
|
+
import { bloom } from 'three/addons/tsl/display/BloomNode.js'
|
|
4
|
+
import { gaussianBlur } from 'three/addons/tsl/display/GaussianBlurNode.js'
|
|
5
|
+
|
|
6
|
+
// Same scene and camera, with a black background only while rendering the light layer.
|
|
7
|
+
class BeamPass extends PassNode {
|
|
8
|
+
constructor(scene, camera) { super(PassNode.COLOR, scene, camera, { samples: 0, depthBuffer: false }); this.name = 'Projector beams'; this._clear = new Color() }
|
|
9
|
+
updateBefore(frame) {
|
|
10
|
+
const scene = this.scene, renderer = frame.renderer
|
|
11
|
+
const background = scene.background, backgroundNode = scene.backgroundNode, alpha = renderer.getClearAlpha()
|
|
12
|
+
renderer.getClearColor(this._clear)
|
|
13
|
+
try {
|
|
14
|
+
scene.background = null; scene.backgroundNode = null; renderer.setClearColor(0x000000, 0)
|
|
15
|
+
super.updateBefore(frame)
|
|
16
|
+
} finally {
|
|
17
|
+
scene.background = background; scene.backgroundNode = backgroundNode; renderer.setClearColor(this._clear, alpha)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @typedef {import('../core/LaserShowBase.js').LaserShowBase} LaserShowBase */
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} LaserShowPipelineOptions
|
|
25
|
+
* @property {number} [beamLayer] Layer reserved for beam meshes, 1 to 31 (default 31).
|
|
26
|
+
* @property {number} [strength] Bloom strength (default 15).
|
|
27
|
+
* @property {number} [radius] Bloom radius (default 1).
|
|
28
|
+
* @property {number} [threshold] Bloom luminance threshold (default 0).
|
|
29
|
+
* @property {number} [samples] MSAA samples of the scene pass (default 4).
|
|
30
|
+
* @property {boolean} [inspect] Label the passes for the r185 Inspector (default false).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Optional post-processing owned by the host application. Uses its scene, camera and renderer.
|
|
35
|
+
* Reserves beamLayer (default 31); all other objects keep their normal camera layers.
|
|
36
|
+
* One pipeline can serve multiple registered LaserShow objects in the same scene.
|
|
37
|
+
*/
|
|
38
|
+
export class LaserShowPipeline {
|
|
39
|
+
/**
|
|
40
|
+
* @param {import('three/webgpu').WebGPURenderer} renderer
|
|
41
|
+
* @param {import('three').Scene} scene
|
|
42
|
+
* @param {import('three').Camera} camera
|
|
43
|
+
* @param {LaserShowPipelineOptions} [options]
|
|
44
|
+
*/
|
|
45
|
+
constructor(renderer, scene, camera, { beamLayer = 31, strength = 15, radius = 1, threshold = 0, samples = 4, inspect = false } = {}) {
|
|
46
|
+
if (!Number.isInteger(beamLayer) || beamLayer < 1 || beamLayer > 31) throw new RangeError('beamLayer must be an integer from 1 to 31.')
|
|
47
|
+
this.renderer = renderer; this.camera = camera; this.beamLayer = beamLayer
|
|
48
|
+
/** @type {Map<LaserShowBase, { mask: number, opacity: unknown, onDispose: () => void }>} */
|
|
49
|
+
this.shows = new Map()
|
|
50
|
+
this.disposed = false; this._withBeams = false
|
|
51
|
+
const baseLayers = new Layers(); baseLayers.mask = camera.layers.mask; baseLayers.disable(beamLayer)
|
|
52
|
+
this._baseLayers = baseLayers
|
|
53
|
+
const beamLayers = new Layers(); beamLayers.set(beamLayer)
|
|
54
|
+
this.scenePass = pass(scene, camera, { samples }).setLayers(baseLayers)
|
|
55
|
+
this._near = uniform(camera.near); this._far = uniform(camera.far)
|
|
56
|
+
const depth = this.scenePass.getTextureNode('depth').sample(screenUV)
|
|
57
|
+
this._sceneViewZ = (camera.isPerspectiveCamera ? perspectiveDepthToViewZ : orthographicDepthToViewZ)(depth, this._near, this._far)
|
|
58
|
+
this.beamPass = new BeamPass(scene, camera).setLayers(beamLayers).setResolutionScale(0.5)
|
|
59
|
+
const label = (node, name) => inspect ? node.toInspector(name) : node
|
|
60
|
+
const sceneColor = label(this.scenePass.getTextureNode('output'), 'Scene / projection')
|
|
61
|
+
this.glow = bloom(sceneColor, strength, radius, threshold)
|
|
62
|
+
this.blur = gaussianBlur(this.beamPass.getTextureNode('output'), vec2(1, 1), 3)
|
|
63
|
+
const glowColor = label(this.glow, 'Laser bloom'), beamColor = label(this.blur, 'Soft projector beams')
|
|
64
|
+
this.projectionOutput = sceneColor.add(glowColor)
|
|
65
|
+
const protect = float(1).sub(smoothstep(0.025, 0.3, max(this.projectionOutput.r, max(this.projectionOutput.g, this.projectionOutput.b))))
|
|
66
|
+
const haze = vec3(1).sub(beamColor.rgb.negate().exp()).mul(0.14).mul(protect)
|
|
67
|
+
this.beamOutput = vec4(this.projectionOutput.rgb.add(haze), this.projectionOutput.a)
|
|
68
|
+
this.pipeline = new RenderPipeline(renderer, this.projectionOutput)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @param {LaserShowBase} show @returns {this} */
|
|
72
|
+
add(show) {
|
|
73
|
+
if (this.shows.has(show)) return this
|
|
74
|
+
if (!show.isLaserShow || show.disposed) throw new TypeError('Expected a live LaserShow.')
|
|
75
|
+
if (!show.beams.material.isNodeMaterial) throw new TypeError('Expected a LaserShow from the three-ilda entry; three-ilda/webgl shows need the webgl pipeline.')
|
|
76
|
+
if (show.beams.userData.laserPipeline) throw new Error('This LaserShow already belongs to another LaserShowPipeline.')
|
|
77
|
+
const saved = { mask: show.beams.layers.mask, opacity: show.beams.material.opacityNode, onDispose: () => this.remove(show) }
|
|
78
|
+
this.shows.set(show, saved); show.beams.userData.laserPipeline = this
|
|
79
|
+
show.beams.layers.set(this.beamLayer)
|
|
80
|
+
// Beam geometry is drawn separately, but the main scene depth still occludes it.
|
|
81
|
+
const difference = this._sceneViewZ.sub(positionView.z)
|
|
82
|
+
show.beams.material.opacityNode = float(1).sub(smoothstep(0.002, 0.015, difference))
|
|
83
|
+
show.beams.material.needsUpdate = true; show.addEventListener('dispose', saved.onDispose)
|
|
84
|
+
return this
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @param {LaserShowBase} show @returns {this} */
|
|
88
|
+
remove(show) {
|
|
89
|
+
const saved = this.shows.get(show)
|
|
90
|
+
if (!saved) return this
|
|
91
|
+
show.beams.layers.mask = saved.mask; show.beams.material.opacityNode = saved.opacity
|
|
92
|
+
show.beams.material.needsUpdate = true; delete show.beams.userData.laserPipeline
|
|
93
|
+
show.removeEventListener('dispose', saved.onDispose); this.shows.delete(show)
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render() {
|
|
98
|
+
if (this.disposed) return
|
|
99
|
+
this._near.value = this.camera.near; this._far.value = this.camera.far
|
|
100
|
+
this._baseLayers.mask = this.camera.layers.mask; this._baseLayers.disable(this.beamLayer)
|
|
101
|
+
let beams = false
|
|
102
|
+
for (const show of this.shows.keys()) {
|
|
103
|
+
let visible = show.beams.visible && show.beamIntensity > 0
|
|
104
|
+
for (let parent = show; parent; parent = parent.parent) visible &&= parent.visible
|
|
105
|
+
if (visible) { beams = true; break }
|
|
106
|
+
}
|
|
107
|
+
if (beams !== this._withBeams) {
|
|
108
|
+
this._withBeams = beams; this.pipeline.outputNode = beams ? this.beamOutput : this.projectionOutput; this.pipeline.needsUpdate = true
|
|
109
|
+
}
|
|
110
|
+
this.pipeline.render()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
dispose() {
|
|
114
|
+
if (this.disposed) return
|
|
115
|
+
for (const show of [...this.shows.keys()]) this.remove(show)
|
|
116
|
+
this.pipeline.dispose(); this.scenePass.dispose(); this.beamPass.dispose(); this.glow.dispose(); this.blur.dispose()
|
|
117
|
+
this.disposed = true
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { AdditiveBlending, DoubleSide, ShaderMaterial, Vector2, Vector3 } from 'three'
|
|
2
|
+
import { LaserShowBase } from '../core/LaserShowBase.js'
|
|
3
|
+
|
|
4
|
+
const LINE_VERTEX = /* glsl */`
|
|
5
|
+
uniform vec3 scale;
|
|
6
|
+
varying vec3 vColor;
|
|
7
|
+
void main() {
|
|
8
|
+
vColor = color;
|
|
9
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position * scale, 1.0);
|
|
10
|
+
}`
|
|
11
|
+
|
|
12
|
+
const LINE_FRAGMENT = /* glsl */`
|
|
13
|
+
uniform float intensity;
|
|
14
|
+
varying vec3 vColor;
|
|
15
|
+
void main() { gl_FragColor = vec4(vColor * intensity, 1.0); }`
|
|
16
|
+
|
|
17
|
+
const BEAM_VERTEX = /* glsl */`
|
|
18
|
+
uniform vec3 scale, origin;
|
|
19
|
+
attribute float beamAlong;
|
|
20
|
+
varying vec3 vColor, vWorld;
|
|
21
|
+
varying float vAlong, vViewZ;
|
|
22
|
+
void main() {
|
|
23
|
+
vColor = color; vAlong = beamAlong;
|
|
24
|
+
vec4 world = modelMatrix * vec4(mix(origin, position * scale, beamAlong), 1.0);
|
|
25
|
+
vec4 view = viewMatrix * world;
|
|
26
|
+
vWorld = world.xyz; vViewZ = view.z;
|
|
27
|
+
gl_Position = projectionMatrix * view;
|
|
28
|
+
}`
|
|
29
|
+
|
|
30
|
+
// DEPTH_FADE is defined by LaserShowPipeline, which also provides the scene depth uniforms.
|
|
31
|
+
const BEAM_FRAGMENT = /* glsl */`
|
|
32
|
+
uniform float clock, beamIntensity;
|
|
33
|
+
varying vec3 vColor, vWorld;
|
|
34
|
+
varying float vAlong, vViewZ;
|
|
35
|
+
#ifdef DEPTH_FADE
|
|
36
|
+
#include <packing>
|
|
37
|
+
uniform sampler2D tDepth;
|
|
38
|
+
uniform vec2 resolution;
|
|
39
|
+
uniform float cameraNear, cameraFar;
|
|
40
|
+
uniform bool orthographic;
|
|
41
|
+
#endif
|
|
42
|
+
// 3D gradient noise, the role mx_noise_float plays in the TSL material; values stay roughly within [-1, 1].
|
|
43
|
+
vec3 hash(vec3 p) {
|
|
44
|
+
p = vec3(dot(p, vec3(127.1, 311.7, 74.7)), dot(p, vec3(269.5, 183.3, 246.1)), dot(p, vec3(113.5, 271.9, 124.6)));
|
|
45
|
+
return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
|
|
46
|
+
}
|
|
47
|
+
float noise(vec3 p) {
|
|
48
|
+
vec3 i = floor(p), f = fract(p), u = f * f * (3.0 - 2.0 * f);
|
|
49
|
+
return mix(
|
|
50
|
+
mix(mix(dot(hash(i), f), dot(hash(i + vec3(1.0, 0.0, 0.0)), f - vec3(1.0, 0.0, 0.0)), u.x),
|
|
51
|
+
mix(dot(hash(i + vec3(0.0, 1.0, 0.0)), f - vec3(0.0, 1.0, 0.0)), dot(hash(i + vec3(1.0, 1.0, 0.0)), f - vec3(1.0, 1.0, 0.0)), u.x), u.y),
|
|
52
|
+
mix(mix(dot(hash(i + vec3(0.0, 0.0, 1.0)), f - vec3(0.0, 0.0, 1.0)), dot(hash(i + vec3(1.0, 0.0, 1.0)), f - vec3(1.0, 0.0, 1.0)), u.x),
|
|
53
|
+
mix(dot(hash(i + vec3(0.0, 1.0, 1.0)), f - vec3(0.0, 1.0, 1.0)), dot(hash(i + vec3(1.0, 1.0, 1.0)), f - vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z);
|
|
54
|
+
}
|
|
55
|
+
void main() {
|
|
56
|
+
float envelope = smoothstep(0.0, 0.025, vAlong) * (1.0 - smoothstep(0.78, 1.0, vAlong));
|
|
57
|
+
float haze = 0.7 + 0.3 * noise(vWorld * 1.4 + vec3(clock * 0.08, clock * -0.12, 0.0));
|
|
58
|
+
float alpha = 1.0;
|
|
59
|
+
#ifdef DEPTH_FADE
|
|
60
|
+
float depth = texture2D(tDepth, gl_FragCoord.xy / resolution).x;
|
|
61
|
+
float sceneViewZ = orthographic ? orthographicDepthToViewZ(depth, cameraNear, cameraFar) : perspectiveDepthToViewZ(depth, cameraNear, cameraFar);
|
|
62
|
+
alpha = 1.0 - smoothstep(0.002, 0.015, sceneViewZ - vViewZ);
|
|
63
|
+
#endif
|
|
64
|
+
gl_FragColor = vec4(vColor * haze * envelope * beamIntensity, alpha);
|
|
65
|
+
}`
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Laser show for THREE.WebGLRenderer (the WebGL 2 version): ShaderMaterials with the same math as the TSL variant.
|
|
69
|
+
* Playback, buffers and the public API live in LaserShowBase.
|
|
70
|
+
*/
|
|
71
|
+
export class LaserShow extends LaserShowBase {
|
|
72
|
+
_createMaterials() {
|
|
73
|
+
const scale = { value: new Vector3() }, origin = { value: new Vector3() }, clock = { value: 0 }, intensity = { value: 1 }, beamIntensity = { value: 0.5 }
|
|
74
|
+
const line = new ShaderMaterial({
|
|
75
|
+
uniforms: { scale, intensity }, vertexShader: LINE_VERTEX, fragmentShader: LINE_FRAGMENT,
|
|
76
|
+
vertexColors: true, transparent: true, depthWrite: false, blending: AdditiveBlending,
|
|
77
|
+
})
|
|
78
|
+
const beam = new ShaderMaterial({
|
|
79
|
+
uniforms: { scale, origin, clock, beamIntensity, tDepth: { value: null }, resolution: { value: new Vector2(1, 1) }, cameraNear: { value: 0.1 }, cameraFar: { value: 1000 }, orthographic: { value: false } },
|
|
80
|
+
vertexShader: BEAM_VERTEX, fragmentShader: BEAM_FRAGMENT,
|
|
81
|
+
vertexColors: true, transparent: true, depthWrite: false, blending: AdditiveBlending, side: DoubleSide, forceSinglePass: true,
|
|
82
|
+
})
|
|
83
|
+
return { line, beam, scale, origin, clock, intensity, beamIntensity }
|
|
84
|
+
}
|
|
85
|
+
}
|