@3driseai/3drise-viewer 0.1.22 → 0.1.24
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/dist/components/CreatedObject.d.ts.map +1 -1
- package/dist/components/CreatedObject.js +6 -1
- package/dist/components/LightObject.d.ts.map +1 -1
- package/dist/components/LightObject.js +24 -13
- package/dist/components/ModelPrimitive.d.ts.map +1 -1
- package/dist/components/ModelPrimitive.js +4 -6
- package/dist/components/ObjectNode.js +1 -1
- package/dist/components/ParticlesGenerator.d.ts +3 -1
- package/dist/components/ParticlesGenerator.d.ts.map +1 -1
- package/dist/components/ParticlesGenerator.js +2 -2
- package/dist/components/ProjectLoader.js +1 -1
- package/dist/components/SceneBuilder.d.ts.map +1 -1
- package/dist/components/SceneBuilder.js +5 -1
- package/dist/components/candle/Candle.d.ts +14 -0
- package/dist/components/candle/Candle.d.ts.map +1 -0
- package/dist/components/candle/Candle.js +249 -0
- package/dist/components/candle/candleGeometry.d.ts +23 -0
- package/dist/components/candle/candleGeometry.d.ts.map +1 -0
- package/dist/components/candle/candleGeometry.js +73 -0
- package/dist/components/candle/candleMaterials.d.ts +24 -0
- package/dist/components/candle/candleMaterials.d.ts.map +1 -0
- package/dist/components/candle/candleMaterials.js +433 -0
- package/dist/components/candle/flameMotion.d.ts +64 -0
- package/dist/components/candle/flameMotion.d.ts.map +1 -0
- package/dist/components/candle/flameMotion.js +138 -0
- package/dist/components/candle/index.d.ts +9 -0
- package/dist/components/candle/index.d.ts.map +1 -0
- package/dist/components/candle/index.js +6 -0
- package/dist/components/fluorescentTube/FluorescentTube.d.ts +14 -0
- package/dist/components/fluorescentTube/FluorescentTube.d.ts.map +1 -0
- package/dist/components/fluorescentTube/FluorescentTube.js +262 -0
- package/dist/components/fluorescentTube/index.d.ts +5 -0
- package/dist/components/fluorescentTube/index.d.ts.map +1 -0
- package/dist/components/fluorescentTube/index.js +3 -0
- package/dist/components/fluorescentTube/tubeMaterials.d.ts +34 -0
- package/dist/components/fluorescentTube/tubeMaterials.d.ts.map +1 -0
- package/dist/components/fluorescentTube/tubeMaterials.js +185 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +4 -0
- package/dist/components/ledArray/LedArray.d.ts +14 -0
- package/dist/components/ledArray/LedArray.d.ts.map +1 -0
- package/dist/components/ledArray/LedArray.js +243 -0
- package/dist/components/ledArray/index.d.ts +7 -0
- package/dist/components/ledArray/index.d.ts.map +1 -0
- package/dist/components/ledArray/index.js +4 -0
- package/dist/components/ledArray/ledLayout.d.ts +22 -0
- package/dist/components/ledArray/ledLayout.d.ts.map +1 -0
- package/dist/components/ledArray/ledLayout.js +59 -0
- package/dist/components/ledArray/ledMaterials.d.ts +14 -0
- package/dist/components/ledArray/ledMaterials.d.ts.map +1 -0
- package/dist/components/ledArray/ledMaterials.js +151 -0
- package/dist/components/lightBulb/LightBulb.d.ts.map +1 -1
- package/dist/components/lightBulb/LightBulb.js +10 -13
- package/dist/components/particles/BioluminescentWakeParticles.d.ts +3 -1
- package/dist/components/particles/BioluminescentWakeParticles.d.ts.map +1 -1
- package/dist/components/particles/BioluminescentWakeParticles.js +7 -3
- package/dist/components/particles/BrownianDustParticles.d.ts +3 -1
- package/dist/components/particles/BrownianDustParticles.d.ts.map +1 -1
- package/dist/components/particles/BrownianDustParticles.js +7 -3
- package/dist/components/particles/ChladniSandParticles.d.ts +3 -1
- package/dist/components/particles/ChladniSandParticles.d.ts.map +1 -1
- package/dist/components/particles/ChladniSandParticles.js +7 -3
- package/dist/components/particles/ElectronOrbitalsParticles.d.ts +3 -1
- package/dist/components/particles/ElectronOrbitalsParticles.d.ts.map +1 -1
- package/dist/components/particles/ElectronOrbitalsParticles.js +7 -3
- package/dist/components/particles/FaradayPowderParticles.d.ts +3 -1
- package/dist/components/particles/FaradayPowderParticles.d.ts.map +1 -1
- package/dist/components/particles/FaradayPowderParticles.js +7 -3
- package/dist/components/particles/GrinderSparksParticles.d.ts +3 -1
- package/dist/components/particles/GrinderSparksParticles.d.ts.map +1 -1
- package/dist/components/particles/GrinderSparksParticles.js +7 -3
- package/dist/components/particles/NeonRainParticles.d.ts +3 -1
- package/dist/components/particles/NeonRainParticles.d.ts.map +1 -1
- package/dist/components/particles/NeonRainParticles.js +7 -3
- package/dist/components/particles/TidalStreamParticles.d.ts +3 -1
- package/dist/components/particles/TidalStreamParticles.d.ts.map +1 -1
- package/dist/components/particles/TidalStreamParticles.js +7 -3
- package/dist/components/spotBeam/SpotBeam.d.ts +14 -0
- package/dist/components/spotBeam/SpotBeam.d.ts.map +1 -0
- package/dist/components/spotBeam/SpotBeam.js +244 -0
- package/dist/components/spotBeam/beamPatterns.d.ts +11 -0
- package/dist/components/spotBeam/beamPatterns.d.ts.map +1 -0
- package/dist/components/spotBeam/beamPatterns.js +106 -0
- package/dist/components/spotBeam/index.d.ts +7 -0
- package/dist/components/spotBeam/index.d.ts.map +1 -0
- package/dist/components/spotBeam/index.js +4 -0
- package/dist/components/spotBeam/spotBeamMaterials.d.ts +16 -0
- package/dist/components/spotBeam/spotBeamMaterials.d.ts.map +1 -0
- package/dist/components/spotBeam/spotBeamMaterials.js +307 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useLightConfig.d.ts +25 -0
- package/dist/hooks/useLightConfig.d.ts.map +1 -0
- package/dist/hooks/useLightConfig.js +125 -0
- package/dist/hooks/useLightLive.d.ts.map +1 -1
- package/dist/hooks/useLightLive.js +12 -2
- package/dist/hooks/useShadowApplication.d.ts +4 -4
- package/dist/hooks/useShadowApplication.d.ts.map +1 -1
- package/dist/hooks/useShadowApplication.js +25 -10
- package/package.json +2 -2
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// candleMaterials — the flame volume, the wax, the wick and the halo
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// The flame is raymarched. A candle flame is not one glowing blob: it has three
|
|
6
|
+
// zones, and rendering only the third is what makes fake candles look fake.
|
|
7
|
+
//
|
|
8
|
+
// - a blue base around the wick, where there is plenty of oxygen. That light
|
|
9
|
+
// is chemiluminescence from CH and C2 radicals, NOT blackbody, so it is a
|
|
10
|
+
// separate additive term rather than the cold end of a temperature ramp;
|
|
11
|
+
// - a dark inner zone just above the wick: vaporised wax, dense but not yet
|
|
12
|
+
// burning, so high density and almost no emission;
|
|
13
|
+
// - the luminous yellow body: incandescent soot, a greybody, which is what the
|
|
14
|
+
// temperature ramp actually describes.
|
|
15
|
+
//
|
|
16
|
+
// The viewer has no tone mapping, so the shader carries its own knee (extended
|
|
17
|
+
// Reinhard, per channel). Without it the whole flame clips to a white blob and
|
|
18
|
+
// the blue base disappears entirely — the single biggest risk to this looking
|
|
19
|
+
// real, so the knee is not optional.
|
|
20
|
+
//
|
|
21
|
+
import { AdditiveBlending, BackSide, Color, DoubleSide, MeshBasicMaterial, MeshPhysicalMaterial, ShaderMaterial, Vector2, Vector3, } from 'three';
|
|
22
|
+
export const FLAME_MODES = ['volumetric', 'billboard'];
|
|
23
|
+
const OUTPUT = /* glsl */ `
|
|
24
|
+
#include <tonemapping_fragment>
|
|
25
|
+
#include <colorspace_fragment>
|
|
26
|
+
`;
|
|
27
|
+
/** Value noise + 3-octave fbm, shared by the flame and the smoke. */
|
|
28
|
+
const NOISE = /* glsl */ `
|
|
29
|
+
float hash13(vec3 p) {
|
|
30
|
+
p = fract(p * 0.1031);
|
|
31
|
+
p += dot(p, p.zyx + 31.32);
|
|
32
|
+
return fract((p.x + p.y) * p.z);
|
|
33
|
+
}
|
|
34
|
+
float vnoise(vec3 p) {
|
|
35
|
+
vec3 i = floor(p), f = fract(p);
|
|
36
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
37
|
+
float n000 = hash13(i), n100 = hash13(i + vec3(1,0,0));
|
|
38
|
+
float n010 = hash13(i + vec3(0,1,0)), n110 = hash13(i + vec3(1,1,0));
|
|
39
|
+
float n001 = hash13(i + vec3(0,0,1)), n101 = hash13(i + vec3(1,0,1));
|
|
40
|
+
float n011 = hash13(i + vec3(0,1,1)), n111 = hash13(i + vec3(1,1,1));
|
|
41
|
+
return mix(mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y),
|
|
42
|
+
mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y), f.z);
|
|
43
|
+
}
|
|
44
|
+
float fbm(vec3 p) {
|
|
45
|
+
return vnoise(p) * 0.55 + vnoise(p * 2.03) * 0.3 + vnoise(p * 4.11) * 0.15;
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
/**
|
|
49
|
+
* The flame's shape and emission, in the box's own space (-0.5..0.5 on each
|
|
50
|
+
* axis, the flame standing on the bottom face). Shared by both modes so the
|
|
51
|
+
* billboard is the same flame, only cheaper.
|
|
52
|
+
*/
|
|
53
|
+
const FLAME_FIELD = /* glsl */ `
|
|
54
|
+
uniform float uTime;
|
|
55
|
+
uniform float uLevel; // 0 out, 1 established
|
|
56
|
+
uniform float uPulse; // buoyancy oscillation, around 1
|
|
57
|
+
uniform float uPhase; // that oscillation's phase, for the travelling wave
|
|
58
|
+
uniform vec2 uLean; // steady wind bend, box units
|
|
59
|
+
uniform vec2 uSwayDir; // the plane it is swaying in; drifts
|
|
60
|
+
uniform float uSway; // amplitude of the undulation running up the flame
|
|
61
|
+
uniform float uSeed; // per candle, so two of them never match
|
|
62
|
+
uniform float uScroll; // how fast the eddies travel up. 0 freezes them
|
|
63
|
+
uniform float uShed; // how hard the neck pinches before it sheds
|
|
64
|
+
uniform float uShedPhase;
|
|
65
|
+
uniform float uDetach; // how far wind has lifted the root off the wick
|
|
66
|
+
uniform float uStretch;
|
|
67
|
+
uniform float uTurb;
|
|
68
|
+
uniform float uRadius; // flame half-width at its widest, box units
|
|
69
|
+
uniform float uTop; // where the tip sits in the box, 0..1
|
|
70
|
+
uniform float uGap; // quench gap: the flame does not touch the wick
|
|
71
|
+
uniform float uBlueHeight; // fraction of the flame that reads blue
|
|
72
|
+
uniform float uBlue;
|
|
73
|
+
uniform float uBlueBias;
|
|
74
|
+
uniform float uSoot;
|
|
75
|
+
uniform vec3 uFringe; // cooling soot at the edges and the tip
|
|
76
|
+
uniform vec3 uBody; // greybody at this temperature
|
|
77
|
+
uniform vec3 uCore; // white-hot core
|
|
78
|
+
uniform vec3 uBlueColor;
|
|
79
|
+
uniform float uGain;
|
|
80
|
+
uniform float uWhite; // knee white point
|
|
81
|
+
|
|
82
|
+
/** Teardrop: rounded at the base, widest about a quarter up, pinching out. */
|
|
83
|
+
float profile(float y) {
|
|
84
|
+
return pow(max(y, 0.0), 0.38) * pow(max(1.0 - y, 0.0), 1.0) / 0.40;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The flame's centreline. Wind bends it, but the part that makes a candle
|
|
89
|
+
* look alive is the second term: the oscillation's phase lags with height,
|
|
90
|
+
* so a wave travels UP the flame and it undulates like a ribbon instead of
|
|
91
|
+
* tilting rigidly. A flame that only leans is the giveaway of a fake one.
|
|
92
|
+
*/
|
|
93
|
+
vec2 centreline(float y) {
|
|
94
|
+
vec2 bend = uLean * pow(y, 1.6);
|
|
95
|
+
float ph = uPhase - y * 4.2;
|
|
96
|
+
// Elliptical, in a plane that drifts: a flame that rocks along one fixed
|
|
97
|
+
// axis reads as a mechanism.
|
|
98
|
+
vec2 d1 = uSwayDir;
|
|
99
|
+
vec2 d2 = vec2(-uSwayDir.y, uSwayDir.x);
|
|
100
|
+
vec2 wave = (d1 * sin(ph) + d2 * sin(ph * 0.61 + uSeed) * 0.55)
|
|
101
|
+
* uSway * smoothstep(0.04, 0.7, y);
|
|
102
|
+
return bend + wave;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Density, plus the two coordinates the emission needs. */
|
|
106
|
+
float flameDensity(vec3 p, out float y, out float r) {
|
|
107
|
+
float top = min(uTop * uLevel * uStretch, 1.0);
|
|
108
|
+
y = (p.y + 0.5) / max(top, 1e-3);
|
|
109
|
+
r = 999.0;
|
|
110
|
+
if (y <= 0.0 || y >= 1.0 || uLevel <= 0.001) return 0.0;
|
|
111
|
+
|
|
112
|
+
vec2 q = p.xz - centreline(y);
|
|
113
|
+
|
|
114
|
+
// The neck: a pinch that travels up the flame and repeats, so the tip
|
|
115
|
+
// necks down and sheds a parcel of gas rather than sitting still.
|
|
116
|
+
float pinchAt = 0.42 + fract(uShedPhase) * 0.62;
|
|
117
|
+
float pinch = uShed * exp(-pow((y - pinchAt) / 0.11, 2.0));
|
|
118
|
+
float width = uRadius * uLevel * profile(y) * (1.0 - pinch);
|
|
119
|
+
if (width <= 1e-4) return 0.0;
|
|
120
|
+
r = length(q) / width;
|
|
121
|
+
if (r > 2.2) return 0.0;
|
|
122
|
+
|
|
123
|
+
// Wind lifts the root clear of the wick before it blows the flame out.
|
|
124
|
+
float root = smoothstep(0.0, 0.04 + uDetach * 0.26, y);
|
|
125
|
+
|
|
126
|
+
// Eddies travel upward, and the gas accelerates as it rises, so the
|
|
127
|
+
// noise coordinate is compressed with height: features stretch as they
|
|
128
|
+
// go, which is what the upper half of a real flame does.
|
|
129
|
+
float turb = uTurb * smoothstep(0.08, 0.8, y);
|
|
130
|
+
float ny = pow(y, 0.72) * 6.5 - uTime * uScroll + uSeed * 13.0;
|
|
131
|
+
float n = (fbm(vec3(q * 24.0 + uSeed * 2.7, ny)) - 0.5) * 2.0;
|
|
132
|
+
return smoothstep(1.0, 0.42, r + n * turb) * root;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The blue collar: low down and on the oxygen-rich surface, wrapping under
|
|
136
|
+
the yellow rather than sitting inside it. */
|
|
137
|
+
float collar(float y, float r) {
|
|
138
|
+
// As the blue reaches further up it also fills in: a flame with plenty of
|
|
139
|
+
// oxygen is blue through, not just at its skin.
|
|
140
|
+
float shell = smoothstep(0.60 * (1.0 - uBlueHeight * 0.85), 1.05, r);
|
|
141
|
+
return smoothstep(max(uBlueHeight, 0.001), 0.0, y) * shell;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
vec3 flameEmission(float d, float y, float r) {
|
|
145
|
+
// The luminous zone starts ABOVE the wick — there is a quench layer the
|
|
146
|
+
// flame does not reach into, and the gap is clearly visible in a macro
|
|
147
|
+
// shot as a dark collar under the yellow.
|
|
148
|
+
float yl = clamp((y - uGap) / max(1.0 - uGap, 1e-3), 0.0, 1.0);
|
|
149
|
+
|
|
150
|
+
// Soot has to be carried up and heated before it glows, so the luminous
|
|
151
|
+
// zone is the UPPER half. Getting this backwards makes a flame read as a
|
|
152
|
+
// glowing blob on a stick.
|
|
153
|
+
float hot = smoothstep(0.05, 0.42, yl) * (1.0 - smoothstep(0.70, 1.06, yl));
|
|
154
|
+
// The dark zone: unburnt vapour on the axis, just above the wick.
|
|
155
|
+
float dark = (1.0 - smoothstep(0.0, 0.78, r)) * (1.0 - smoothstep(0.02, 0.34, yl));
|
|
156
|
+
float lum = d * (0.10 + 2.0 * hot) * (1.0 - 0.92 * dark)
|
|
157
|
+
* smoothstep(0.0, max(0.3, uBlueHeight * 1.15), yl);
|
|
158
|
+
|
|
159
|
+
// Three stops, not two. Soot cools at the outer fringe and at the tip,
|
|
160
|
+
// and drops to a deep red long before it stops glowing — leaving that
|
|
161
|
+
// out is what makes a flame look like an airbrushed gradient.
|
|
162
|
+
// Mostly radial: the fringe is the cool outside of the sheet, not a
|
|
163
|
+
// horizontal stripe across it.
|
|
164
|
+
float cool = clamp(smoothstep(0.58, 1.05, r) * 0.8 + smoothstep(0.80, 1.1, yl) * 0.45, 0.0, 1.0);
|
|
165
|
+
vec3 col = mix(uBody, uFringe, cool);
|
|
166
|
+
col = mix(col, uCore, smoothstep(0.5, 1.4, lum));
|
|
167
|
+
|
|
168
|
+
// The blue is faint next to the yellow and the first thing the knee
|
|
169
|
+
// eats, so it is weighted up rather than left proportionate and invisible.
|
|
170
|
+
float blue = uBlue * uBlueBias * collar(y, r) * d;
|
|
171
|
+
return col * lum + uBlueColor * blue * 7.5;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Extended Reinhard, per channel: the core desaturates to white, the rest
|
|
175
|
+
stays where it was. Stands in for the tone mapping the viewer does not do. */
|
|
176
|
+
vec3 knee(vec3 c) {
|
|
177
|
+
float w = max(uWhite, 1.0);
|
|
178
|
+
return c * (1.0 + c / (w * w)) / (1.0 + c);
|
|
179
|
+
}
|
|
180
|
+
`;
|
|
181
|
+
const FLAME_UNIFORMS = () => ({
|
|
182
|
+
uTime: { value: 0 },
|
|
183
|
+
uLevel: { value: 1 },
|
|
184
|
+
uPulse: { value: 1 },
|
|
185
|
+
uPhase: { value: 0 },
|
|
186
|
+
uLean: { value: new Vector2(0, 0) },
|
|
187
|
+
uSwayDir: { value: new Vector2(1, 0) },
|
|
188
|
+
uSway: { value: 0.02 },
|
|
189
|
+
uSeed: { value: 0 },
|
|
190
|
+
uScroll: { value: 1.4 },
|
|
191
|
+
uShed: { value: 0.18 },
|
|
192
|
+
uShedPhase: { value: 0 },
|
|
193
|
+
uDetach: { value: 0 },
|
|
194
|
+
uGap: { value: 0.05 },
|
|
195
|
+
uBlueHeight: { value: 0.16 },
|
|
196
|
+
uStretch: { value: 1 },
|
|
197
|
+
uTurb: { value: 0.3 },
|
|
198
|
+
uRadius: { value: 0.16 },
|
|
199
|
+
uTop: { value: 0.78 },
|
|
200
|
+
uBlue: { value: 0.7 },
|
|
201
|
+
uBlueBias: { value: 1 },
|
|
202
|
+
uSoot: { value: 0.25 },
|
|
203
|
+
uFringe: { value: new Color(1, 0.26, 0.04) },
|
|
204
|
+
uBody: { value: new Color(1, 0.62, 0.18) },
|
|
205
|
+
uCore: { value: new Color(1, 0.95, 0.82) },
|
|
206
|
+
uBlueColor: { value: new Color(0.14, 0.42, 1) },
|
|
207
|
+
uGain: { value: 5 },
|
|
208
|
+
uWhite: { value: 3.5 },
|
|
209
|
+
uSteps: { value: 24 },
|
|
210
|
+
/** Camera in the box's object space; only the raymarched mode reads it. */
|
|
211
|
+
uCamera: { value: new Vector3() },
|
|
212
|
+
});
|
|
213
|
+
function flameMaterial() {
|
|
214
|
+
return new ShaderMaterial({
|
|
215
|
+
uniforms: FLAME_UNIFORMS(),
|
|
216
|
+
vertexShader: /* glsl */ `
|
|
217
|
+
varying vec3 vLocal;
|
|
218
|
+
void main() {
|
|
219
|
+
vLocal = position;
|
|
220
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
221
|
+
}`,
|
|
222
|
+
fragmentShader: /* glsl */ `
|
|
223
|
+
uniform vec3 uCamera; // camera in the box's object space
|
|
224
|
+
uniform float uSteps;
|
|
225
|
+
varying vec3 vLocal;
|
|
226
|
+
${NOISE}
|
|
227
|
+
${FLAME_FIELD}
|
|
228
|
+
|
|
229
|
+
void main() {
|
|
230
|
+
// Back faces are rendered, so this fragment is where the ray
|
|
231
|
+
// leaves the box; the entry comes from the slab intersection and
|
|
232
|
+
// is clamped to 0 so the camera can sit inside.
|
|
233
|
+
vec3 ro = uCamera;
|
|
234
|
+
vec3 rd = vLocal - ro;
|
|
235
|
+
float far = length(rd);
|
|
236
|
+
rd /= max(far, 1e-6);
|
|
237
|
+
|
|
238
|
+
// March the flame's own bounds, not the whole box. The box has to
|
|
239
|
+
// be big enough for the lean and the undulation, but a ray that
|
|
240
|
+
// spends half its steps on empty air bands badly where the flame
|
|
241
|
+
// is thin — which is exactly where the blue collar lives.
|
|
242
|
+
float topB = min(uTop * uLevel * uStretch, 1.0);
|
|
243
|
+
float halfW = min(uRadius * uLevel + max(abs(uLean.x), abs(uLean.y)) + uSway + 0.03, 0.5);
|
|
244
|
+
vec3 bmin = vec3(-halfW, -0.5, -halfW);
|
|
245
|
+
vec3 bmax = vec3( halfW, -0.5 + topB, halfW);
|
|
246
|
+
|
|
247
|
+
vec3 inv = 1.0 / max(abs(rd), vec3(1e-6)) * sign(rd + 1e-9);
|
|
248
|
+
vec3 t0 = (bmin - ro) * inv;
|
|
249
|
+
vec3 t1 = (bmax - ro) * inv;
|
|
250
|
+
vec3 tlo = min(t0, t1), thi = max(t0, t1);
|
|
251
|
+
float near = max(max(tlo.x, tlo.y), max(tlo.z, 0.0));
|
|
252
|
+
float exit = min(min(thi.x, thi.y), thi.z);
|
|
253
|
+
far = min(far, exit);
|
|
254
|
+
if (near >= far) discard;
|
|
255
|
+
|
|
256
|
+
const int MAX_STEPS = 40;
|
|
257
|
+
float steps = clamp(uSteps, 8.0, float(MAX_STEPS));
|
|
258
|
+
float stepLen = (far - near) / steps;
|
|
259
|
+
// Dither the entry point: 20 steps through a small volume bands
|
|
260
|
+
// badly otherwise, and a flame shows it more than most things.
|
|
261
|
+
// A third of a step of interleaved gradient noise, on the pixel
|
|
262
|
+
// only. Full-width jitter trades banding for a diagonal moiré
|
|
263
|
+
// that is worse than what it fixes; a third is enough to break
|
|
264
|
+
// the rings now that the march is confined to the flame.
|
|
265
|
+
float ign = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
|
|
266
|
+
float t = near + stepLen * (0.5 + (ign - 0.5) * 0.34);
|
|
267
|
+
|
|
268
|
+
vec3 acc = vec3(0.0);
|
|
269
|
+
float trans = 1.0;
|
|
270
|
+
for (int i = 0; i < MAX_STEPS; i++) {
|
|
271
|
+
if (float(i) >= steps || trans < 0.01) break;
|
|
272
|
+
vec3 p = ro + rd * t;
|
|
273
|
+
float y, r;
|
|
274
|
+
float d = flameDensity(p, y, r);
|
|
275
|
+
if (d > 0.002) {
|
|
276
|
+
acc += flameEmission(d, y, r) * stepLen * trans * uGain;
|
|
277
|
+
// Soot absorbs a little, which is what gives a flame a body
|
|
278
|
+
// rather than making it a pure additive ghost.
|
|
279
|
+
trans *= exp(-d * uSoot * stepLen * 14.0);
|
|
280
|
+
}
|
|
281
|
+
t += stepLen;
|
|
282
|
+
}
|
|
283
|
+
if (acc.r + acc.g + acc.b < 1e-4) discard;
|
|
284
|
+
gl_FragColor = vec4(knee(acc), 1.0);
|
|
285
|
+
${OUTPUT}
|
|
286
|
+
}`,
|
|
287
|
+
transparent: true,
|
|
288
|
+
depthWrite: false,
|
|
289
|
+
side: BackSide,
|
|
290
|
+
blending: AdditiveBlending,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/** The same flame on one camera-facing quad: no depth, a fraction of the cost. */
|
|
294
|
+
function flameBillboardMaterial() {
|
|
295
|
+
return new ShaderMaterial({
|
|
296
|
+
uniforms: FLAME_UNIFORMS(),
|
|
297
|
+
vertexShader: /* glsl */ `
|
|
298
|
+
varying vec2 vUv;
|
|
299
|
+
void main() {
|
|
300
|
+
vUv = position.xy;
|
|
301
|
+
vec4 mv = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
302
|
+
float s = length(modelMatrix[0].xyz);
|
|
303
|
+
mv.xy += position.xy * s * 0.5;
|
|
304
|
+
gl_Position = projectionMatrix * mv;
|
|
305
|
+
}`,
|
|
306
|
+
fragmentShader: /* glsl */ `
|
|
307
|
+
varying vec2 vUv;
|
|
308
|
+
${NOISE}
|
|
309
|
+
${FLAME_FIELD}
|
|
310
|
+
void main() {
|
|
311
|
+
// Three slices instead of a march: enough to keep the dark zone
|
|
312
|
+
// and the blue base, without the depth.
|
|
313
|
+
vec3 acc = vec3(0.0);
|
|
314
|
+
for (int i = 0; i < 3; i++) {
|
|
315
|
+
float z = (float(i) - 1.0) * 0.11;
|
|
316
|
+
float y, r;
|
|
317
|
+
vec3 p = vec3(vUv.x * 0.5, vUv.y * 0.5, z);
|
|
318
|
+
float d = flameDensity(p, y, r);
|
|
319
|
+
if (d > 0.002) acc += flameEmission(d, y, r) * uGain * 0.17;
|
|
320
|
+
}
|
|
321
|
+
if (acc.r + acc.g + acc.b < 1e-4) discard;
|
|
322
|
+
gl_FragColor = vec4(knee(acc), 1.0);
|
|
323
|
+
${OUTPUT}
|
|
324
|
+
}`,
|
|
325
|
+
transparent: true,
|
|
326
|
+
depthWrite: false,
|
|
327
|
+
blending: AdditiveBlending,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** The wisp left behind for a few seconds after it is snuffed. */
|
|
331
|
+
function smokeMaterial() {
|
|
332
|
+
return new ShaderMaterial({
|
|
333
|
+
uniforms: {
|
|
334
|
+
uTime: { value: 0 },
|
|
335
|
+
uAge: { value: 0 },
|
|
336
|
+
uColor: { value: new Color(0.6, 0.6, 0.62) },
|
|
337
|
+
},
|
|
338
|
+
// A plain quad, turned to face the camera about Y by the component. A
|
|
339
|
+
// view-space billboard would ignore the mesh's own height and park the
|
|
340
|
+
// wisp wherever the X scale put it.
|
|
341
|
+
vertexShader: /* glsl */ `
|
|
342
|
+
varying vec2 vUv;
|
|
343
|
+
void main() {
|
|
344
|
+
vUv = position.xy;
|
|
345
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
346
|
+
}`,
|
|
347
|
+
fragmentShader: /* glsl */ `
|
|
348
|
+
uniform float uTime;
|
|
349
|
+
uniform float uAge;
|
|
350
|
+
uniform vec3 uColor;
|
|
351
|
+
varying vec2 vUv;
|
|
352
|
+
${NOISE}
|
|
353
|
+
void main() {
|
|
354
|
+
// The quad's bottom sits on the wick, so the wisp has to start at
|
|
355
|
+
// y = 0 or it detaches and floats in mid-air.
|
|
356
|
+
float y = vUv.y * 0.5 + 0.5;
|
|
357
|
+
float rise = clamp(uAge * 0.55, 0.0, 1.0);
|
|
358
|
+
float band = smoothstep(0.0, 0.03, y) * (1.0 - smoothstep(rise, rise + 0.4, y));
|
|
359
|
+
float wobble = (fbm(vec3(vUv * 3.0, uTime * 0.5)) - 0.5) * (0.1 + y * 0.55);
|
|
360
|
+
// Thin. Snuffing a candle gives a thread of smoke, not a plume.
|
|
361
|
+
float w = 0.02 + y * 0.15;
|
|
362
|
+
float a = band * exp(-pow((vUv.x - wobble) / w, 2.0));
|
|
363
|
+
a *= (1.0 - smoothstep(1.6, 3.4, uAge)) * 0.33;
|
|
364
|
+
if (a < 0.002) discard;
|
|
365
|
+
gl_FragColor = vec4(uColor, a);
|
|
366
|
+
${OUTPUT}
|
|
367
|
+
}`,
|
|
368
|
+
transparent: true,
|
|
369
|
+
depthWrite: false,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function haloMaterial() {
|
|
373
|
+
return new ShaderMaterial({
|
|
374
|
+
uniforms: {
|
|
375
|
+
uColor: { value: new Color(1, 0.7, 0.35) },
|
|
376
|
+
uStrength: { value: 0 },
|
|
377
|
+
uRadius: { value: 0.3 },
|
|
378
|
+
},
|
|
379
|
+
vertexShader: /* glsl */ `
|
|
380
|
+
uniform float uRadius;
|
|
381
|
+
varying vec2 vUv;
|
|
382
|
+
void main() {
|
|
383
|
+
vec4 mv = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
384
|
+
mv.xy += position.xy * length(modelMatrix[0].xyz) * uRadius;
|
|
385
|
+
vUv = position.xy;
|
|
386
|
+
gl_Position = projectionMatrix * mv;
|
|
387
|
+
}`,
|
|
388
|
+
fragmentShader: /* glsl */ `
|
|
389
|
+
uniform vec3 uColor;
|
|
390
|
+
uniform float uStrength;
|
|
391
|
+
varying vec2 vUv;
|
|
392
|
+
void main() {
|
|
393
|
+
float d = length(vUv);
|
|
394
|
+
// Tight. A wide soft halo round a candle reads as fog, not flame.
|
|
395
|
+
float a = exp(-d * d * 7.0);
|
|
396
|
+
gl_FragColor = vec4(uColor * uStrength * a, 1.0);
|
|
397
|
+
${OUTPUT}
|
|
398
|
+
}`,
|
|
399
|
+
transparent: true,
|
|
400
|
+
depthWrite: false,
|
|
401
|
+
blending: AdditiveBlending,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
export function createCandleMaterials() {
|
|
405
|
+
return {
|
|
406
|
+
flame: flameMaterial(),
|
|
407
|
+
flameBillboard: flameBillboardMaterial(),
|
|
408
|
+
smoke: smokeMaterial(),
|
|
409
|
+
wax: new MeshPhysicalMaterial({
|
|
410
|
+
color: 0xf3e7d2, roughness: 0.42, transmission: 0.6, thickness: 0.05,
|
|
411
|
+
ior: 1.45, attenuationColor: new Color(0.95, 0.82, 0.62), attenuationDistance: 0.06,
|
|
412
|
+
sheen: 0.7, sheenColor: new Color(1, 0.94, 0.85), sheenRoughness: 0.6,
|
|
413
|
+
clearcoat: 0.15, clearcoatRoughness: 0.5, side: DoubleSide,
|
|
414
|
+
}),
|
|
415
|
+
melt: new MeshPhysicalMaterial({
|
|
416
|
+
color: 0xf6ead6, roughness: 0.1, transmission: 0.75, thickness: 0.02,
|
|
417
|
+
ior: 1.45, clearcoat: 1, clearcoatRoughness: 0.05, side: DoubleSide,
|
|
418
|
+
}),
|
|
419
|
+
wick: new MeshBasicMaterial({ color: 0x1a1512, toneMapped: false }),
|
|
420
|
+
ember: new MeshBasicMaterial({ color: 0xff6a1e, toneMapped: false }),
|
|
421
|
+
halo: haloMaterial(),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
export function disposeCandleMaterials(m) {
|
|
425
|
+
m.flame.dispose();
|
|
426
|
+
m.flameBillboard.dispose();
|
|
427
|
+
m.smoke.dispose();
|
|
428
|
+
m.wax.dispose();
|
|
429
|
+
m.melt.dispose();
|
|
430
|
+
m.wick.dispose();
|
|
431
|
+
m.ember.dispose();
|
|
432
|
+
m.halo.dispose();
|
|
433
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface FlameMotionConfig {
|
|
2
|
+
/** Depth of the brightness dip, 0-1. Light output only — not shape. */
|
|
3
|
+
flicker: number;
|
|
4
|
+
/** How much the flame moves at all, 0-1. 0 freezes the shape. */
|
|
5
|
+
movement: number;
|
|
6
|
+
/** Carrier rate in Hz. Real candles sit around 10-12 whatever their size. */
|
|
7
|
+
flickerHz: number;
|
|
8
|
+
/** Air movement, 0-1: lean, stretch, turbulence and eventually guttering. */
|
|
9
|
+
draught: number;
|
|
10
|
+
/** Seconds for the wick to catch and the flame to establish. */
|
|
11
|
+
ignite: number;
|
|
12
|
+
/** Whether the candle is lit. */
|
|
13
|
+
on: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface FlameMotionState {
|
|
16
|
+
/** 0 = out, 1 = established. Drives size and output together. */
|
|
17
|
+
level: number;
|
|
18
|
+
/** Height multiplier from the buoyancy pulse, around 1. */
|
|
19
|
+
pulse: number;
|
|
20
|
+
/** Lean of the flame's tip, in flame-height units. */
|
|
21
|
+
leanX: number;
|
|
22
|
+
leanZ: number;
|
|
23
|
+
/** Vertical stretch; draught pulls a flame long and thin. */
|
|
24
|
+
stretch: number;
|
|
25
|
+
/** How turbulent the shape noise is, 0-1. */
|
|
26
|
+
turbulence: number;
|
|
27
|
+
/** How fast the eddies travel up the flame. 0 freezes them. */
|
|
28
|
+
scroll: number;
|
|
29
|
+
/** Luminous output, lagging the shape a little, as the real thing does. */
|
|
30
|
+
output: number;
|
|
31
|
+
/** How blue the flame reads: high while it is catching, low once established. */
|
|
32
|
+
blueBias: number;
|
|
33
|
+
/** Seconds since it was snuffed, for the smoke wisp. -1 when lit or long out. */
|
|
34
|
+
smokeAge: number;
|
|
35
|
+
/** Carrier phase, so the shader can run the wave up the flame. */
|
|
36
|
+
phaseOut: number;
|
|
37
|
+
/** Amplitude of that undulation, in flame-height units. */
|
|
38
|
+
sway: number;
|
|
39
|
+
/** The plane it is swaying in, as a unit vector. Wanders. */
|
|
40
|
+
swayX: number;
|
|
41
|
+
swayZ: number;
|
|
42
|
+
/** How hard the neck pinches before the tip sheds. */
|
|
43
|
+
shed: number;
|
|
44
|
+
/** Where that pinch has travelled to, 0-1. */
|
|
45
|
+
shedPhase: number;
|
|
46
|
+
/** Fixed per candle, so two of them never move identically. */
|
|
47
|
+
noiseSeed: number;
|
|
48
|
+
/** How far wind has lifted the flame's root off the wick, 0-1. */
|
|
49
|
+
detach: number;
|
|
50
|
+
phase: number;
|
|
51
|
+
pinks: number[][];
|
|
52
|
+
windAngle: number;
|
|
53
|
+
swayAngle: number;
|
|
54
|
+
shedT: number;
|
|
55
|
+
shedPeriod: number;
|
|
56
|
+
shedStrength: number;
|
|
57
|
+
wasOn: boolean;
|
|
58
|
+
since: number;
|
|
59
|
+
gutter: number;
|
|
60
|
+
seed: number;
|
|
61
|
+
}
|
|
62
|
+
export declare function createFlameMotion(on: boolean): FlameMotionState;
|
|
63
|
+
export declare function stepFlameMotion(s: FlameMotionState, cfg: FlameMotionConfig, deltaSeconds: number): FlameMotionState;
|
|
64
|
+
//# sourceMappingURL=flameMotion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flameMotion.d.ts","sourceRoot":"","sources":["../../../src/components/candle/flameMotion.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,iBAAiB;IAC9B,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,EAAE,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC7B,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IAEjB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;IAGf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CAChB;AAMD,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,OAAO,GAAG,gBAAgB,CAgB/D;AAmBD,wBAAgB,eAAe,CAAC,CAAC,EAAE,gBAAgB,EAAE,GAAG,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,GAAG,gBAAgB,CAsGnH"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const TWO_PI = Math.PI * 2;
|
|
2
|
+
const clamp01 = (v) => Math.min(Math.max(v, 0), 1);
|
|
3
|
+
const rand = () => Math.random() * 2 - 1;
|
|
4
|
+
export function createFlameMotion(on) {
|
|
5
|
+
const swayAngle = Math.random() * TWO_PI;
|
|
6
|
+
return {
|
|
7
|
+
level: 0, pulse: 1, leanX: 0, leanZ: 0, stretch: 1, turbulence: 0.25, scroll: 1.4,
|
|
8
|
+
output: 0, blueBias: 1, smokeAge: -1,
|
|
9
|
+
phaseOut: 0, sway: 0.02, swayX: Math.cos(swayAngle), swayZ: Math.sin(swayAngle),
|
|
10
|
+
shed: 0.18, shedPhase: 0, noiseSeed: Math.random() * 100, detach: 0,
|
|
11
|
+
phase: Math.random() * TWO_PI,
|
|
12
|
+
// One channel each for lean, frequency, amplitude and sway direction.
|
|
13
|
+
pinks: [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
|
|
14
|
+
windAngle: Math.random() * TWO_PI,
|
|
15
|
+
swayAngle,
|
|
16
|
+
shedT: 0.2, shedPeriod: 0.35, shedStrength: 1,
|
|
17
|
+
wasOn: on, since: 0, gutter: 0,
|
|
18
|
+
seed: Math.random() * 1000,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Pink-ish noise: three leaky integrators at spaced cutoffs, summed. Cheap, and
|
|
23
|
+
* it gives the slow drift-and-return of air in a room rather than the twitch of
|
|
24
|
+
* white noise. Cutoffs roughly 0.35 / 1.4 / 5 Hz.
|
|
25
|
+
*/
|
|
26
|
+
function pink(s, dt, channel) {
|
|
27
|
+
const p = s.pinks[channel];
|
|
28
|
+
const k = [0.35, 1.4, 5.0];
|
|
29
|
+
let sum = 0;
|
|
30
|
+
for (let i = 0; i < 3; i++) {
|
|
31
|
+
const a = 1 - Math.exp(-dt * k[i] * TWO_PI);
|
|
32
|
+
p[i] += (rand() - p[i]) * a;
|
|
33
|
+
sum += p[i] / (i + 1);
|
|
34
|
+
}
|
|
35
|
+
return sum * 0.85;
|
|
36
|
+
}
|
|
37
|
+
export function stepFlameMotion(s, cfg, deltaSeconds) {
|
|
38
|
+
const dt = Math.min(Math.max(deltaSeconds, 0), 0.1);
|
|
39
|
+
const draught = clamp01(cfg.draught);
|
|
40
|
+
const flicker = clamp01(cfg.flicker);
|
|
41
|
+
const move = clamp01(cfg.movement);
|
|
42
|
+
if (cfg.on !== s.wasOn) {
|
|
43
|
+
s.wasOn = cfg.on;
|
|
44
|
+
s.since = 0;
|
|
45
|
+
s.seed = Math.random() * 1000;
|
|
46
|
+
if (!cfg.on)
|
|
47
|
+
s.smokeAge = 0;
|
|
48
|
+
}
|
|
49
|
+
s.since += dt;
|
|
50
|
+
if (s.smokeAge >= 0) {
|
|
51
|
+
s.smokeAge += dt;
|
|
52
|
+
if (s.smokeAge > 3.5)
|
|
53
|
+
s.smokeAge = -1;
|
|
54
|
+
}
|
|
55
|
+
// --- the carrier, with a wandering rate and amplitude --------------------
|
|
56
|
+
// The frequency drift is what breaks the loop: with a fixed rate the two
|
|
57
|
+
// sines below re-phase on a cycle of a few seconds and the eye finds it.
|
|
58
|
+
const rateDrift = 1 + pink(s, dt, 1) * 0.3 * move;
|
|
59
|
+
const hz = Math.max(cfg.flickerHz, 1) * (1 + draught * 0.35) * rateDrift;
|
|
60
|
+
s.phase += dt * hz * TWO_PI;
|
|
61
|
+
if (s.phase > 1e6)
|
|
62
|
+
s.phase -= 1e6;
|
|
63
|
+
const ampDrift = 0.6 + clamp01(pink(s, dt, 2) * 0.5 + 0.5) * 0.85;
|
|
64
|
+
// Golden ratio, not a near-rational one: the two components never line up.
|
|
65
|
+
const wobble = (Math.sin(s.phase) * 0.62
|
|
66
|
+
+ Math.sin(s.phase * 1.618034 + s.seed) * 0.24
|
|
67
|
+
+ pink(s, dt, 0) * 0.34) * ampDrift;
|
|
68
|
+
s.pulse = 1 + wobble * move * 0.17;
|
|
69
|
+
s.phaseOut = s.phase;
|
|
70
|
+
// --- guttering ----------------------------------------------------------
|
|
71
|
+
// A rare, real event: the flame is momentarily starved and nearly goes out.
|
|
72
|
+
// Rate climbs with the square of the draught, so a still room never gutters.
|
|
73
|
+
if (s.gutter > 0)
|
|
74
|
+
s.gutter = Math.max(s.gutter - dt / 0.45, 0);
|
|
75
|
+
else if (cfg.on && draught > 0.25 && Math.random() < dt * draught * draught * 1.6)
|
|
76
|
+
s.gutter = 1;
|
|
77
|
+
const gutterDip = s.gutter > 0 ? 1 - Math.sin(s.gutter * Math.PI) * 0.72 : 1;
|
|
78
|
+
// --- ignition and snuffing ---------------------------------------------
|
|
79
|
+
if (!cfg.on) {
|
|
80
|
+
s.level += (0 - s.level) * (1 - Math.exp(-dt / 0.1));
|
|
81
|
+
if (s.level < 1e-3)
|
|
82
|
+
s.level = 0;
|
|
83
|
+
s.blueBias = 1;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const ignite = Math.max(cfg.ignite, 0);
|
|
87
|
+
if (ignite <= 0)
|
|
88
|
+
s.level = 1;
|
|
89
|
+
else {
|
|
90
|
+
// A wick catches small and blue, flares slightly past full, settles.
|
|
91
|
+
const t = s.since / ignite;
|
|
92
|
+
const target = t >= 1 ? 1 : 1 - Math.exp(-t * 3.4);
|
|
93
|
+
const flare = t < 1 ? Math.sin(Math.min(t, 1) * Math.PI) * 0.12 : 0;
|
|
94
|
+
s.level += (Math.min(target + flare, 1.12) - s.level) * (1 - Math.exp(-dt * 9));
|
|
95
|
+
}
|
|
96
|
+
// Blue dominates while the flame is small: less soot, more oxygen.
|
|
97
|
+
s.blueBias = 1 + Math.max(0, 1 - s.level) * 2.2;
|
|
98
|
+
}
|
|
99
|
+
// --- lean, stretch, turbulence -----------------------------------------
|
|
100
|
+
// No wind direction knob: each candle picks its own and it drifts, which is
|
|
101
|
+
// what a room does anyway and one less thing to set.
|
|
102
|
+
s.windAngle += pink(s, dt, 3) * dt * 0.55;
|
|
103
|
+
const wander = pink(s, dt, 0) * (0.06 + draught * 0.34) * move;
|
|
104
|
+
const push = draught * 0.55;
|
|
105
|
+
s.leanX += (Math.cos(s.windAngle) * push + wander - s.leanX) * (1 - Math.exp(-dt * 6));
|
|
106
|
+
s.leanZ += (Math.sin(s.windAngle) * push + wander * 0.7 - s.leanZ) * (1 - Math.exp(-dt * 5));
|
|
107
|
+
s.stretch = 1 + draught * 0.9 + wobble * move * 0.1;
|
|
108
|
+
// Frozen noise still has shape, so a still flame is irregular rather than a
|
|
109
|
+
// perfect cone — it just stops moving.
|
|
110
|
+
s.turbulence = (0.16 + draught * 0.95) * (0.35 + 0.65 * move);
|
|
111
|
+
s.scroll = (1.4 + s.turbulence * 2.4) * move;
|
|
112
|
+
// --- the undulation running up the flame --------------------------------
|
|
113
|
+
// The plane it sways in drifts, so the flame does not rock back and forth
|
|
114
|
+
// along one fixed axis.
|
|
115
|
+
s.swayAngle += pink(s, dt, 3) * dt * 2.4 * move;
|
|
116
|
+
s.swayX = Math.cos(s.swayAngle);
|
|
117
|
+
s.swayZ = Math.sin(s.swayAngle);
|
|
118
|
+
s.sway = (0.022 + draught * 0.17) * move * ampDrift;
|
|
119
|
+
// --- the tip sheds, on its own schedule ---------------------------------
|
|
120
|
+
// Event-driven rather than a fixed phase march: each shed gets its own
|
|
121
|
+
// interval and strength, so the tip never lets go on a beat.
|
|
122
|
+
s.shedT -= dt * move;
|
|
123
|
+
if (s.shedT <= 0) {
|
|
124
|
+
s.shedPeriod = 0.16 + Math.random() * 0.55;
|
|
125
|
+
s.shedT = s.shedPeriod;
|
|
126
|
+
s.shedStrength = 0.45 + Math.random() * 1.0;
|
|
127
|
+
}
|
|
128
|
+
s.shedPhase = clamp01(1 - s.shedT / Math.max(s.shedPeriod, 1e-3));
|
|
129
|
+
s.shed = (0.12 + draught * 0.4) * move * s.shedStrength;
|
|
130
|
+
// Hard wind lifts the flame clear of the wick before it puts it out.
|
|
131
|
+
s.detach = Math.max(0, (draught - 0.5) / 0.5) * (0.5 + 0.5 * wobble);
|
|
132
|
+
// --- luminous output ----------------------------------------------------
|
|
133
|
+
// The light comes from the whole glowing volume, so it lags the shape and
|
|
134
|
+
// swings less than the height does. ~35 ms is about right.
|
|
135
|
+
const target = s.level * gutterDip * (1 + wobble * flicker * 0.12);
|
|
136
|
+
s.output += (target - s.output) * (1 - Math.exp(-dt / 0.035));
|
|
137
|
+
return s;
|
|
138
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { default as Candle } from './Candle';
|
|
2
|
+
export type { CandleProps } from './Candle';
|
|
3
|
+
export { createCandleMaterials, disposeCandleMaterials, FLAME_MODES } from './candleMaterials';
|
|
4
|
+
export type { CandleMaterials, FlameMode } from './candleMaterials';
|
|
5
|
+
export { buildCandle, CANDLE_BODIES } from './candleGeometry';
|
|
6
|
+
export type { CandleBody, CandleGeometry } from './candleGeometry';
|
|
7
|
+
export { createFlameMotion, stepFlameMotion } from './flameMotion';
|
|
8
|
+
export type { FlameMotionConfig, FlameMotionState } from './flameMotion';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/candle/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,UAAU,CAAC;AAC7C,YAAY,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/F,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACnE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// A candle: a raymarched flame with a real point light, and the wax under it
|
|
2
|
+
// (config.type 'candle').
|
|
3
|
+
export { default as Candle } from './Candle.js';
|
|
4
|
+
export { createCandleMaterials, disposeCandleMaterials, FLAME_MODES } from './candleMaterials.js';
|
|
5
|
+
export { buildCandle, CANDLE_BODIES } from './candleGeometry.js';
|
|
6
|
+
export { createFlameMotion, stepFlameMotion } from './flameMotion.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AnimationOptions, CreatedObjectSettings, MouseMoveDomain } from '@3driseai/3drise-core';
|
|
3
|
+
import type { ObjectReadyProps } from '../../types/objectReady';
|
|
4
|
+
export type FluorescentTubeProps = ObjectReadyProps & {
|
|
5
|
+
/** Scene object id: the light controller finds the live-edit handle by it. */
|
|
6
|
+
id?: string;
|
|
7
|
+
config: Record<string, any>;
|
|
8
|
+
animations?: AnimationOptions[];
|
|
9
|
+
mouseMove?: MouseMoveDomain;
|
|
10
|
+
settings: CreatedObjectSettings | Record<string, any>;
|
|
11
|
+
};
|
|
12
|
+
declare const FluorescentTube: React.FC<FluorescentTubeProps>;
|
|
13
|
+
export default FluorescentTube;
|
|
14
|
+
//# sourceMappingURL=FluorescentTube.d.ts.map
|