three-environment-shadows 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/index.d.ts +140 -0
- package/index.js +7 -0
- package/package.json +60 -4
package/README.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# three-environment-shadows
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use environment as a source of light and _shadows_ in your Three.js scene.
|
|
4
|
+
Extracts light sources from environment map automatically - no need for any more of the manually set PointLights and SunLights 🤲
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
import * as Three from 'three';
|
|
8
|
+
import { UltraHDRLoader } from 'three/addons/loaders/UltraHDRLoader.js';
|
|
9
|
+
import { EnvironmentShadowLights } from 'three-environment-shadows';
|
|
10
|
+
|
|
11
|
+
renderer.shadowMap.enabled = true;
|
|
12
|
+
|
|
13
|
+
const hdri = await new UltraHDRLoader().loadAsync('./environment.jpg');
|
|
14
|
+
hdri.mapping = Three.EquirectangularReflectionMapping;
|
|
15
|
+
|
|
16
|
+
scene.background = hdri;
|
|
17
|
+
scene.environment = hdri;
|
|
18
|
+
|
|
19
|
+
const shadows = new EnvironmentShadowLights(scene);
|
|
20
|
+
shadows.bounds.set(new Three.Vector3(0, 1, 0), 6);
|
|
21
|
+
|
|
22
|
+
mesh.castShadow = true;
|
|
23
|
+
mesh.receiveShadow = true;
|
|
24
|
+
```
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Color,
|
|
3
|
+
DataTexture,
|
|
4
|
+
DirectionalLight,
|
|
5
|
+
Group,
|
|
6
|
+
Object3D,
|
|
7
|
+
Object3DEventMap,
|
|
8
|
+
Scene,
|
|
9
|
+
Sphere,
|
|
10
|
+
Texture,
|
|
11
|
+
Vector3,
|
|
12
|
+
} from 'three';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One part of an extracted light.
|
|
16
|
+
*/
|
|
17
|
+
export interface LightPart {
|
|
18
|
+
/** Unit vector from the scene towards the part, in the space of the environment map. */
|
|
19
|
+
direction: Vector3;
|
|
20
|
+
/** The color, normalized so its largest component is `1`. */
|
|
21
|
+
color: Color;
|
|
22
|
+
/** The irradiance, usable as `DirectionalLight.intensity`. */
|
|
23
|
+
intensity: number;
|
|
24
|
+
/** Half-angle (in radians) of the cone the part emits from. */
|
|
25
|
+
coneAngle: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A light source that was found in an environment map.
|
|
30
|
+
*/
|
|
31
|
+
export interface ExtractedLight extends LightPart {
|
|
32
|
+
/** Fraction of the total environment energy emitted by this light. */
|
|
33
|
+
energyFraction: number;
|
|
34
|
+
/**
|
|
35
|
+
* `partitions[ n - 1 ]` divides the light into `n` parts (median cut). The energies of the
|
|
36
|
+
* parts add up to the energy of the light. `partitions[ 0 ]` holds the light as one part.
|
|
37
|
+
*/
|
|
38
|
+
partitions: LightPart[][];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ExtractionResult {
|
|
42
|
+
/** The extracted lights, strongest first. */
|
|
43
|
+
lights: ExtractedLight[];
|
|
44
|
+
/** `0` if the environment emits equally in all directions, `1` if all light comes from one direction. */
|
|
45
|
+
directionality: number;
|
|
46
|
+
/** Fraction of the total environment energy emitted by the extracted lights. */
|
|
47
|
+
dominance: number;
|
|
48
|
+
/** The radiance (luminance) above which texels are part of a light. */
|
|
49
|
+
threshold: number;
|
|
50
|
+
/** A copy of the environment map without the extracted lights, or `null`. */
|
|
51
|
+
texture: DataTexture | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ExtractOptions {
|
|
55
|
+
/** The maximum number of lights. Default `2`. */
|
|
56
|
+
maxLights?: number;
|
|
57
|
+
/** Texels brighter than this factor times the mean radiance are part of a light. Default `8`. */
|
|
58
|
+
threshold?: number;
|
|
59
|
+
/** The minimum fraction of the total energy a light must emit. Default `0.05`. */
|
|
60
|
+
minEnergy?: number;
|
|
61
|
+
/** The minimum elevation (in radians) of a light, in the space of the environment map. Default `-Math.PI / 2`. */
|
|
62
|
+
minElevation?: number;
|
|
63
|
+
/** The maximum number of parts per light, see `ExtractedLight.partitions`. Default `4`. */
|
|
64
|
+
samples?: number;
|
|
65
|
+
/** The width of the grid used to group bright texels into regions. Default `512`. */
|
|
66
|
+
resolution?: number;
|
|
67
|
+
/** Whether to create a copy of the environment map without the lights. Default `true`. */
|
|
68
|
+
removeLights?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Finds the dominant light sources of an equirectangular environment map.
|
|
73
|
+
*/
|
|
74
|
+
export class EnvironmentLightExtractor {
|
|
75
|
+
static extract(texture: Texture, options?: ExtractOptions): Promise<ExtractionResult>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface EnvironmentShadowLightsOptions {
|
|
79
|
+
/** The maximum number of lights. Default `2`. */
|
|
80
|
+
maxLights?: number;
|
|
81
|
+
/** Texels brighter than this factor times the mean radiance are part of a light. Default `8`. */
|
|
82
|
+
threshold?: number;
|
|
83
|
+
/** The minimum fraction of the total energy a light must emit. Default `0.05`. */
|
|
84
|
+
minEnergy?: number;
|
|
85
|
+
/** The minimum elevation (in radians) of a light, in the space of the environment map. Default `-Math.PI / 2`. */
|
|
86
|
+
minElevation?: number;
|
|
87
|
+
/** The width of the grid used to group bright texels into regions. Default `512`. */
|
|
88
|
+
resolution?: number;
|
|
89
|
+
/** The maximum number of shadow casting lights per light source. Default `4`. */
|
|
90
|
+
samples?: number;
|
|
91
|
+
/** The maximum number of shadow maps of all light sources. Default `4`. */
|
|
92
|
+
maxShadowMaps?: number;
|
|
93
|
+
/** The shadow map size of each light. Default `1024`. */
|
|
94
|
+
mapSize?: number;
|
|
95
|
+
/** The typical distance between shadow casters and receivers, relative to the bounds radius. Default `0.1`. */
|
|
96
|
+
relativeOccluderDistance?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface EnvironmentShadowLightsEventMap extends Object3DEventMap {
|
|
100
|
+
/** The light sources changed, see `EnvironmentShadowLights.result`. */
|
|
101
|
+
change: {};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Casts shadows from the light sources of `scene.environment`.
|
|
106
|
+
*/
|
|
107
|
+
export class EnvironmentShadowLights extends Group<EnvironmentShadowLightsEventMap> {
|
|
108
|
+
/** Constructs the group and adds it to the scene. */
|
|
109
|
+
constructor(scene?: Scene | null, options?: EnvironmentShadowLightsOptions);
|
|
110
|
+
|
|
111
|
+
readonly isEnvironmentShadowLights: true;
|
|
112
|
+
|
|
113
|
+
maxLights: number;
|
|
114
|
+
threshold: number;
|
|
115
|
+
minEnergy: number;
|
|
116
|
+
minElevation: number;
|
|
117
|
+
resolution: number;
|
|
118
|
+
samples: number;
|
|
119
|
+
maxShadowMaps: number;
|
|
120
|
+
mapSize: number;
|
|
121
|
+
relativeOccluderDistance: number;
|
|
122
|
+
|
|
123
|
+
/** Set to `true` to find the light sources again, e.g. after a change of the options. */
|
|
124
|
+
needsUpdate: boolean;
|
|
125
|
+
|
|
126
|
+
/** The area that receives shadows, in world space. */
|
|
127
|
+
bounds: Sphere;
|
|
128
|
+
|
|
129
|
+
/** The light sources of the environment map, or `null` before they are found. */
|
|
130
|
+
readonly result: Omit<ExtractionResult, 'texture'> | null;
|
|
131
|
+
|
|
132
|
+
/** The target of all lights, at the center of the bounds. */
|
|
133
|
+
readonly target: Object3D;
|
|
134
|
+
|
|
135
|
+
/** The lights. The shadow casting lights come first. */
|
|
136
|
+
readonly lights: DirectionalLight[];
|
|
137
|
+
|
|
138
|
+
/** Puts the original environment map back, frees the GPU-related resources and removes the group. */
|
|
139
|
+
dispose(): void;
|
|
140
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Color, RGBAFormat, RGBFormat, FloatType, HalfFloatType, UnsignedByteType, SRGBColorSpace, LinearSRGBColorSpace, NoColorSpace, DataUtils, DataTexture, Vector3, Group, Sphere, Object3D, DirectionalLight } from 'three';
|
|
2
|
+
|
|
3
|
+
const _color=new Color,_rgb={r:0,g:0,b:0};class EnvironmentLightExtractor{static async extract(t,e={}){const{maxLights:n=2,threshold:o=8,minEnergy:r=.05,minElevation:a=-Math.PI/2,samples:i=4,resolution:l=512,removeLights:c=!0}=e,s=readSource(t),h=await runAnalysis({data:s.data,width:s.width,height:s.height,channels:s.channels,encoding:s.encoding,decode:s.decode,flipY:t.flipY,maxLights:n,thresholdFactor:o,minEnergy:r,minElevation:a,maxParts:i,resolution:l});h.clipped&&console.warn("THREE.EnvironmentLightExtractor: Light texels are clipped in the 8-bit texture. Light intensities are too low.");const f=t=>({direction:(new Vector3).fromArray(t.direction),color:(new Color).fromArray(t.color),intensity:t.intensity,coneAngle:t.coneAngle}),p=h.lights.map(t=>{const e=t.partitions.map(t=>t.map(f));return {...f(t),energyFraction:t.energyFraction,partitions:e}});let g=null;return c&&(g=s.createTexture(h.data),removeSelectedLights(s,g.image.data,h)),{lights:p,directionality:h.directionality,dominance:h.dominance,threshold:h.threshold,texture:g}}}function runAnalysis(t){if("undefined"==typeof Worker)return Promise.resolve(analyze(t));const e=`const analyze = ${analyze.toString()};\n\n self.onmessage = (event) => {\n const output = analyze(event.data);\n self.postMessage(output, [output.data.buffer, output.cellSelected.buffer]);\n };`,n=URL.createObjectURL(new Blob([e],{type:"application/javascript"}));return new Promise((e,o)=>{const r=new Worker(n),a=()=>{r.terminate(),URL.revokeObjectURL(n);};r.onmessage=t=>{a(),e(t.data);},r.onerror=t=>{a(),o(new Error("THREE.EnvironmentLightExtractor: "+t.message));},r.postMessage(t,[t.data.buffer]);})}function analyze(t){const{data:e,width:n,height:o,channels:r,flipY:a,maxLights:i,thresholdFactor:l,minEnergy:c,minElevation:s,maxParts:h,resolution:f}=t;let p=t.decode;if("half"===t.encoding){p=new Float32Array(65536);for(let t=0;t<65536;t++){const e=t>>10&31,n=1023&t;let o;o=0===e?n*Math.pow(2,-24):31===e?0===n?1/0:NaN:(1+n/1024)*Math.pow(2,e-15),p[t]=32768&t?-o:o;}}const g="byte"===t.encoding,d=[0,0,0];function u(t,o){const a=(o*n+t)*r;return null===p?(d[0]=e[a],d[1]=e[a+1],d[2]=e[a+2]):(d[0]=p[e[a]],d[1]=p[e[a+1]],d[2]=p[e[a+2]]),.2126*d[0]+.7152*d[1]+.0722*d[2]}const m=new Float64Array(n),y=new Float64Array(n);for(let t=0;t<n;t++){const e=2*((t+.5)/n-.5)*Math.PI;m[t]=Math.sin(e),y[t]=Math.cos(e);}const M=new Float64Array(o),w=new Float64Array(o),x=new Float64Array(o);for(let t=0;t<o;t++){const e=a?1-t/o:t/o,r=a?1-(t+1)/o:(t+1)/o,i=((e+r)/2-.5)*Math.PI;M[t]=Math.sin(i),w[t]=Math.cos(i),x[t]=2*Math.PI/n*Math.abs(Math.sin((r-.5)*Math.PI)-Math.sin((e-.5)*Math.PI));}const E=Math.max(1,Math.min(n,f)),F=Math.max(1,Math.round(E*o/n)),b=E*F,T=new Int32Array(n),S=new Int32Array(o);for(let t=0;t<n;t++)T[t]=Math.floor(t*E/n);for(let t=0;t<o;t++)S[t]=Math.floor(t*F/o);const R={data:e,directionality:0,dominance:0,threshold:0,gridWidth:E,gridHeight:F,cellSelected:new Uint8Array(b),lights:[],clipped:!1},A=new Float32Array(b);let L=0,v=0,B=0,H=0;for(let t=0;t<o;t++){const e=x[t],o=S[t]*E;for(let r=0;r<n;r++){const n=u(r,t);if(!(n>0&&n<1/0))continue;const a=n*e;L+=a,v+=a*w[t]*y[r],B+=a*M[t],H+=a*w[t]*m[r];const i=o+T[r];n>A[i]&&(A[i]=n);}}if(0===L)return R;R.directionality=Math.hypot(v,B,H)/L;const U=l*L/(4*Math.PI);R.threshold=U;const G=new Int32Array(b).fill(-1),C=[],I=[],_=Math.floor(E/2);for(let t=0;t<b;t++){if(G[t]>=0||A[t]<=U)continue;const e=[];for(G[t]=C.length,I.push(t);I.length>0;){const t=I.pop(),n=t%E,o=(t-n)/E;e.push(t);for(let t=-1;t<=1;t++)for(let e=-1;e<=1;e++){if(0===e&&0===t)continue;let r=n+e,a=o+t;(a<0||a>=F)&&(a=a<0?0:F-1,r+=_),r=(r%E+E)%E;const i=a*E+r;G[i]>=0||A[i]<=U||(G[i]=C.length,I.push(i));}}C.push(e);}const D=new Int32Array(b).fill(-1);let P=0;for(const t of C)for(const e of t)D[e]=P++;const z=new Float64Array(7*P),k=new Uint8Array(P);for(let t=0;t<o;t++){const o=x[t],a=S[t]*E;for(let i=0;i<n;i++){const l=D[a+T[i]];if(l<0)continue;const c=u(i,t);if(!(c>U&&c<1/0))continue;const s=1-U/c,h=c*s*o,f=7*l;if(z[f]+=d[0]*s*o,z[f+1]+=d[1]*s*o,z[f+2]+=d[2]*s*o,z[f+3]+=h,z[f+4]+=h*w[t]*y[i],z[f+5]+=h*M[t],z[f+6]+=h*w[t]*m[i],g){const o=(t*n+i)*r;255!==e[o]&&255!==e[o+1]&&255!==e[o+2]||(k[l]=1);}}}function j(t){const e={r:0,g:0,b:0,energy:0,x:0,y:0,z:0};for(const n of t){const t=7*D[n];e.r+=z[t],e.g+=z[t+1],e.b+=z[t+2],e.energy+=z[t+3],e.x+=z[t+4],e.y+=z[t+5],e.z+=z[t+6];}return e}function O(t,e){const{r:n,g:o,b:r,energy:a,x:i,y:l,z:c}=j(t),s=Math.hypot(i,l,c),h=Math.max(n,o,r),f=s/a;return {direction:s>0?[i/s,l/s,c/s]:e,color:[n/h,o/h,r/h],intensity:h,coneAngle:Math.acos(Math.min(1,Math.max(-1,2*f-1)))}}function Y(t){const{x:e,y:n,z:o}=j(t),r=Math.hypot(e,n,o),a=[e/r,n/r,o/r],i=Math.abs(a[1])<.99?[0,1,0]:[1,0,0],l=[i[1]*a[2]-i[2]*a[1],i[2]*a[0]-i[0]*a[2],i[0]*a[1]-i[1]*a[0]],c=Math.hypot(l[0],l[1],l[2]);l[0]/=c,l[1]/=c,l[2]/=c;const s=[a[1]*l[2]-a[2]*l[1],a[2]*l[0]-a[0]*l[2],a[0]*l[1]-a[1]*l[0]],h=[];let f=0,p=0,g=0;for(const e of t){const t=7*D[e],n=z[t+3],o=Math.hypot(z[t+4],z[t+5],z[t+6]),r=z[t+4]/o,a=z[t+5]/o,i=z[t+6]/o,c=r*l[0]+a*l[1]+i*l[2],d=r*s[0]+a*s[1]+i*s[2];h.push({cell:e,e:n,px:c,py:d}),f+=n,p+=n*c,g+=n*d;}p/=f,g/=f;let d=0,u=0,m=0;for(const t of h){const e=t.px-p,n=t.py-g;d+=t.e*e*e,u+=t.e*n*n,m+=t.e*e*n;}const y=.5*Math.atan2(2*m,d-u),M=Math.cos(y),w=Math.sin(y);for(const t of h)t.projection=t.px*M+t.py*w;h.sort((t,e)=>t.projection-e.projection);let x=0,E=0;for(;x<h.length-1&&E+h[x].e<=f/2;)E+=h[x++].e;return x=Math.min(h.length-1,Math.max(1,x)),[h.slice(0,x).map(t=>t.cell),h.slice(x).map(t=>t.cell)]}function N(t,e){const n=[[t]];for(;n.length<h;){const t=n[n.length-1];let e=-1,o=0;for(let n=0;n<t.length;n++){if(t[n].length<2)continue;const{energy:r,x:a,y:i,z:l}=j(t[n]),c=r-Math.hypot(a,i,l);c>o&&(e=n,o=c);}if(e<0)break;const r=t.slice();r.splice(e,1,...Y(t[e])),n.push(r);}return n.map(t=>t.map(t=>O(t,e)))}const W=C.map(t=>j(t).energy),q=C.map((t,e)=>e).sort((t,e)=>W[e]-W[t]),V=Math.sin(s);for(const t of q){if(R.lights.length>=i)break;const e=W[t]/L;if(e<c)break;const n=C[t],o=O(n,[0,1,0]);if(!(o.direction[1]<V)){o.energyFraction=e,o.partitions=N(n,o.direction),R.lights.push(o),R.dominance+=e;for(const t of n)R.cellSelected[t]=1,1===k[D[t]]&&(R.clipped=!0);}}return R}function removeSelectedLights(t,e,n){const{width:o,height:r}=t,{gridWidth:a,gridHeight:i,cellSelected:l,threshold:c}=n,s=t.reader(e),h=t.writer(e);for(let t=0;t<i;t++){const e=Math.ceil(t*r/i),n=Math.ceil((t+1)*r/i);for(let r=0;r<a;r++){if(0===l[t*a+r])continue;const i=Math.ceil(r*o/a),f=Math.ceil((r+1)*o/a);for(let t=e;t<n;t++)for(let e=i;e<f;e++){s(e,t,_rgb);const n=.2126*_rgb.r+.7152*_rgb.g+.0722*_rgb.b;if(!(n>c&&n<1/0))continue;const o=c/n;h(e,t,_rgb.r*o,_rgb.g*o,_rgb.b*o);}}}}function readSource(t){const e=t.image;if(t.isCubeTexture||t.isCompressedTexture||t.isRenderTargetTexture||t.isData3DTexture||t.isDataArrayTexture)throw new Error("THREE.EnvironmentLightExtractor: Only equirectangular data or image textures are supported.");if(null==e)throw new Error("THREE.EnvironmentLightExtractor: The texture has no image data.");let n,o,r,a,i,l;if(void 0!==e.data){if(o=e.width,r=e.height,n=e.data.slice(),i=t.type,l=t.format,l===RGBAFormat)a=4;else {if(l!==RGBFormat)throw new Error("THREE.EnvironmentLightExtractor: Only RGB and RGBA textures are supported.");a=3;}if(i!==FloatType&&i!==HalfFloatType&&i!==UnsignedByteType)throw new Error("THREE.EnvironmentLightExtractor: Only float, half float and unsigned byte textures are supported.")}else o=e.width,r=e.height,n=readImage(e,o,r),a=4,i=UnsignedByteType,l=RGBAFormat;let c=t.colorSpace;c!==SRGBColorSpace&&c!==LinearSRGBColorSpace&&c!==NoColorSpace&&(console.warn("THREE.EnvironmentLightExtractor: Unsupported color space. Texels are read as linear."),c=LinearSRGBColorSpace);const s=c===SRGBColorSpace;let h=null;if(i===UnsignedByteType){h=new Float32Array(256);for(let t=0;t<256;t++)h[t]=s?_color.setRGB(t/255,0,0,SRGBColorSpace).r:t/255;}return {data:n,width:o,height:r,channels:a,encoding:i===FloatType?"float":i===HalfFloatType?"half":"byte",decode:h,reader:function(t){return i===FloatType?(e,n,r)=>{const i=(n*o+e)*a;r.r=t[i],r.g=t[i+1],r.b=t[i+2];}:i===HalfFloatType?(e,n,r)=>{const i=(n*o+e)*a;r.r=DataUtils.fromHalfFloat(t[i]),r.g=DataUtils.fromHalfFloat(t[i+1]),r.b=DataUtils.fromHalfFloat(t[i+2]);}:(e,n,r)=>{const i=(n*o+e)*a;r.r=h[t[i]],r.g=h[t[i+1]],r.b=h[t[i+2]];}},writer:function(t){return i===FloatType?(e,n,r,i,l)=>{const c=(n*o+e)*a;t[c]=r,t[c+1]=i,t[c+2]=l;}:i===HalfFloatType?(e,n,r,i,l)=>{const c=(n*o+e)*a;t[c]=DataUtils.toHalfFloat(r),t[c+1]=DataUtils.toHalfFloat(i),t[c+2]=DataUtils.toHalfFloat(l);}:(e,n,r,i,l)=>{const c=(n*o+e)*a;s&&(_color.setRGB(r,i,l).getRGB(_rgb,SRGBColorSpace),r=_rgb.r,i=_rgb.g,l=_rgb.b),t[c]=Math.round(255*Math.min(1,r)),t[c+1]=Math.round(255*Math.min(1,i)),t[c+2]=Math.round(255*Math.min(1,l));}},createTexture:function(e){const n=new DataTexture(e,o,r,l,i);return n.mapping=t.mapping,n.wrapS=t.wrapS,n.wrapT=t.wrapT,n.magFilter=t.magFilter,n.minFilter=t.minFilter,n.anisotropy=t.anisotropy,n.generateMipmaps=t.generateMipmaps,n.unpackAlignment=t.unpackAlignment,n.colorSpace=t.colorSpace,n.flipY=t.flipY,n.needsUpdate=!0,n}}}function readImage(t,e,n){const o=Math.min(e,4096),r=Math.min(n,4096);let a;"undefined"!=typeof document?(a=document.createElement("canvas"),a.width=o,a.height=r):a=new OffscreenCanvas(o,r);const i=a.getContext("2d",{willReadFrequently:!0}),l=new Uint8Array(e*n*4);for(let a=0;a<n;a+=r)for(let c=0;c<e;c+=o){const s=Math.min(o,e-c),h=Math.min(r,n-a);i.clearRect(0,0,s,h),i.drawImage(t,c,a,s,h,0,0,s,h);const f=i.getImageData(0,0,s,h).data;for(let t=0;t<h;t++)l.set(f.subarray(t*s*4,(t+1)*s*4),4*((a+t)*e+c));}return l}
|
|
4
|
+
|
|
5
|
+
const _direction=new Vector3,_position=new Vector3,getRoot=t=>{let i=t;for(;null!==i.parent;)i=i.parent;return i},isLoaded=t=>{const i=t.image;return null!=i&&(!!Array.isArray(i)||i.width>0&&i.height>0&&(void 0===i.data||i.data.length>0))};class EnvironmentShadowLights extends Group{constructor(t=null,i={}){super(),this.isEnvironmentShadowLights=!0,this.type="EnvironmentShadowLights",this.maxLights=i.maxLights??2,this.threshold=i.threshold??8,this.minEnergy=i.minEnergy??.05,this.minElevation=i.minElevation??-Math.PI/2,this.resolution=i.resolution??512,this.samples=i.samples??4,this.maxShadowMaps=i.maxShadowMaps??4,this.mapSize=i.mapSize??1024,this.relativeOccluderDistance=i.relativeOccluderDistance??.1,this.needsUpdate=!1,this.bounds=new Sphere(new Vector3,10),this.result=null,this.target=new Object3D,this.target.matrixAutoUpdate=!1,this.target.matrixWorldAutoUpdate=!1,this.add(this.target),this.lights=[],this._parts=[],this._source=null,this._sourceVersion=-1,this._residual=null,this._scene=null,this._request=null,this.addEventListener("removed",()=>this._restoreEnvironment()),null!==t&&t.add(this);}updateMatrixWorld(t){super.updateMatrixWorld(t);const i=getRoot(this);i.isScene&&this._syncEnvironment(i);const s=i.isScene?i.environmentRotation:null,e=i.isScene?i.environmentIntensity:1,{center:n,radius:r}=this.bounds;this.target.position.copy(n),this.target.matrix.makeTranslation(n),this.target.matrixWorld.copy(this.target.matrix);const o=2*r/this.mapSize;for(let t=0;t<this.lights.length;t++){const i=this.lights[t],h=this._parts[t];if(_direction.copy(h.direction),null!==s&&_direction.applyEuler(s),_position.copy(_direction).multiplyScalar(2*r).add(n),i.position.copy(_position),i.matrix.makeTranslation(_position),i.matrixWorld.copy(i.matrix),i.intensity=h.intensity*e,i.castShadow){const t=i.shadow.camera;t.left=-r,t.right=r,t.top=r,t.bottom=-r,t.near=r,t.far=3*r,t.updateProjectionMatrix(),i.shadow.normalBias=o;}}}copy(t,i=!0){if(super.copy(t,!1),this.maxLights=t.maxLights,this.threshold=t.threshold,this.minEnergy=t.minEnergy,this.minElevation=t.minElevation,this.resolution=t.resolution,this.samples=t.samples,this.maxShadowMaps=t.maxShadowMaps,this.mapSize=t.mapSize,this.relativeOccluderDistance=t.relativeOccluderDistance,this.bounds.copy(t.bounds),i)for(const i of t.children)i===t.target||t.lights.includes(i)||this.add(i.clone());return this}dispose(){this._restoreEnvironment(),this._clear(),this.removeFromParent();}_syncEnvironment(t){let i=t.environment;if(null!==i&&i===this._residual){if(!this.needsUpdate&&this._source.version===this._sourceVersion)return;i=this._source;}else if(null!==i&&i===this._source&&null!==this._residual&&!this.needsUpdate&&i.version===this._sourceVersion)return t.environment=this._residual,void(this._scene=t);const s=this._request;i===this._source||null!==s&&i===s.texture||this._clear(),null!==i&&isLoaded(i)&&(null===s||s.texture!==i||s.version!==i.version||this.needsUpdate)&&this._extract(t,i);}_extract(t,i){const s={texture:i,version:i.version};this.needsUpdate=!1,this._request=s,EnvironmentLightExtractor.extract(i,{maxLights:this.maxLights,threshold:this.threshold,minEnergy:this.minEnergy,minElevation:this.minElevation,samples:this.samples,resolution:this.resolution}).then(i=>{this._request===s?(this._request=null,this._apply(t,s,i)):i.texture.dispose();},t=>{this._request===s&&console.warn(t.message);});}_apply(t,i,s){const e=this._residual,n=t.environment;this._source=i.texture,this._sourceVersion=i.version,this._residual=s.texture,this.result={lights:s.lights,directionality:s.directionality,dominance:s.dominance,threshold:s.threshold},this._setLights(s.lights),getRoot(this)!==t||n!==this._source&&n!==e||(t.environment=this._residual,this._scene=t),null!==e&&e.dispose(),this.dispatchEvent({type:"change"});}_restoreEnvironment(){const t=this._scene;null!==t&&t.environment===this._residual&&(t.environment=this._source),this._scene=null;}_clear(){this._request=null,null!==this._residual&&this._residual.dispose(),this._source=null,this._sourceVersion=-1,this._residual=null,this._scene=null,null===this.result&&0===this.lights.length||(this.result=null,this._setLights([]),this.dispatchEvent({type:"change"}));}_setLights(t){for(const t of this.lights)this.remove(t),t.dispose();this.lights.length=0,this._parts.length=0;const i=t=>Math.tan(t)*this.relativeOccluderDistance*this.mapSize/2,s=t.map(()=>0);let e=this.maxShadowMaps;for(let i=0;i<t.length&&e>0;i++)s[i]=1,e--;for(let n=0;n<t.length&&e>0;n++){const r=t[n];if(0===s[n]||i(r.coneAngle)<=1)continue;const o=Math.min(Math.min(this.samples,r.partitions.length)-1,e);s[n]+=o,e-=o;}for(let e=0;e<t.length;e++){const n=s[e]>0,r=t[e].partitions[Math.max(1,s[e])-1];for(const t of r){const s=new DirectionalLight(t.color,t.intensity);s.matrixAutoUpdate=!1,s.matrixWorldAutoUpdate=!1,s.target=this.target,n&&(s.castShadow=!0,s.shadow.mapSize.setScalar(this.mapSize),s.shadow.radius=Math.min(8,Math.max(1,i(t.coneAngle)))),this.lights.push(s),this._parts.push(t),this.add(s);}}}}
|
|
6
|
+
|
|
7
|
+
export { EnvironmentLightExtractor, EnvironmentShadowLights };
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-environment-shadows",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Use HDR / EXR / UltraHDR environment maps as source of light and shadows in Three.js.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"import": "./index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"files": [
|
|
16
|
+
"index.js",
|
|
17
|
+
"index.d.ts",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"threejs",
|
|
22
|
+
"three",
|
|
23
|
+
"shadows",
|
|
24
|
+
"environment",
|
|
25
|
+
"envmap",
|
|
26
|
+
"hdr",
|
|
27
|
+
"ibl",
|
|
28
|
+
"lighting",
|
|
29
|
+
"webgl",
|
|
30
|
+
"webgpu"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "rollup --config ./rollup.config.mjs && cd build && npm pack",
|
|
34
|
+
"build:examples": "cd examples && npm run build",
|
|
35
|
+
"dev": "cd examples && npm run dev",
|
|
36
|
+
"format": "prettier --write src test types",
|
|
37
|
+
"test": "vitest run && tsc",
|
|
38
|
+
"test:build": "TEST_BUILD=1 vitest run",
|
|
39
|
+
"release": "npm run build && npm run test:build && cd ./build && npm publish"
|
|
40
|
+
},
|
|
41
|
+
"author": {
|
|
42
|
+
"name": "mjurczyk",
|
|
43
|
+
"url": "https://discourse.threejs.org/u/mjurczyk"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"three": ">=0.163.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@lopatnov/rollup-plugin-uglify": "^2.1.5",
|
|
51
|
+
"@types/three": "^0.187.0",
|
|
52
|
+
"prettier": "^3.9.0",
|
|
53
|
+
"rollup": "^2.79.1",
|
|
54
|
+
"rollup-plugin-copy": "^3.4.0",
|
|
55
|
+
"rollup-plugin-delete": "^2.0.0",
|
|
56
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
57
|
+
"terser": "^5.36.0",
|
|
58
|
+
"three": "^0.187.0",
|
|
59
|
+
"typescript": "^5.9.0",
|
|
60
|
+
"vitest": "^5.1.0"
|
|
61
|
+
}
|
|
62
|
+
}
|