Upgrade to Three.js 0.185.1 (#8853)

- Point lights and spot lights now uses a realistic decay. It's not possible to find the exact same result as before, but you can tweak the intensity and the decay properties. Hemisphere lights, directional lights and ambient lights still work the same as before.
- The 3D particle emitter must be updated to the last version to handle the new 3D renderer world scale property.
- Ensure any 3D extension you use is also updated to the latest version.
This commit is contained in:
D8H
2026-10-03 13:55:50 +02:00
committed by GitHub
parent da3e59c232
commit 7dd4d13a7a
82 changed files with 3377 additions and 1090 deletions
+4
View File
@@ -252,6 +252,7 @@ void Layout::SerializeTo(SerializerElement& element) const {
element.SetAttribute("resourcesUnloading", resourcesUnloading);
element.SetAttribute("disableInputWhenNotFocused",
disableInputWhenNotFocused);
element.SetAttribute("renderer3DWorldScale", renderer3DWorldScale);
editorSettings.SerializeTo(element.AddChild("uiSettings"));
@@ -316,6 +317,8 @@ void Layout::UnserializeFrom(gd::Project& project,
element.GetStringAttribute("resourcesUnloading", "inherit");
disableInputWhenNotFocused =
element.GetBoolAttribute("disableInputWhenNotFocused");
renderer3DWorldScale =
element.GetDoubleAttribute("renderer3DWorldScale", 100);
editorSettings.UnserializeFrom(
element.GetChild("uiSettings", 0, "UISettings"));
@@ -404,6 +407,7 @@ void Layout::Init(const Layout& other) {
resourcesPreloading = other.resourcesPreloading;
resourcesUnloading = other.resourcesUnloading;
disableInputWhenNotFocused = other.disableInputWhenNotFocused;
renderer3DWorldScale = other.renderer3DWorldScale;
initialInstances = other.initialInstances;
layers = other.layers;
variables = other.GetVariables();
+13
View File
@@ -380,6 +380,18 @@ class GD_CORE_API Layout {
* `inherit` (default).
*/
const gd::String& GetResourcesUnloading() const { return resourcesUnloading; }
/**
* Get the world scale used by the 3D renderer.
*/
const double GetRenderer3DWorldScale() const { return renderer3DWorldScale; };
/**
* Set the world scale used by the 3D renderer.
*/
void SetRenderer3DWorldScale(double worldScale) {
renderer3DWorldScale = worldScale;
};
///@}
/** \name Saving and loading
@@ -429,6 +441,7 @@ class GD_CORE_API Layout {
EventsList events; ///< Scene events
gd::EditorSettings editorSettings;
double renderer3DWorldScale = 100;
/**
* Initialize from another layout. Used by copy-ctor and assign-op.
+2 -2
View File
@@ -62,12 +62,12 @@ namespace gdjs {
updatePreRender(target: gdjs.EffectsTarget): any {}
updateDoubleParameter(parameterName: string, value: number): void {
if (parameterName === 'intensity') {
this.light.intensity = value;
this.light.intensity = value * Math.PI;
}
}
getDoubleParameter(parameterName: string): number {
if (parameterName === 'intensity') {
return this.light.intensity;
return this.light.intensity / Math.PI;
}
return 0;
}
@@ -247,14 +247,14 @@ namespace gdjs {
getAnimationFrameWidth(material: THREE.Material) {
const map = (
material as THREE.MeshBasicMaterial | THREE.MeshStandardMaterial
).map;
).map as THREE.Texture<HTMLImageElement>;
return map ? map.image.width : 0;
}
getAnimationFrameHeight(material: THREE.Material) {
const map = (
material as THREE.MeshBasicMaterial | THREE.MeshStandardMaterial
).map;
).map as THREE.Texture<HTMLImageElement>;
return map ? map.image.height : 0;
}
}
+29 -13
View File
@@ -30,6 +30,7 @@ namespace gdjs {
private _light: THREE.DirectionalLight;
private _shadowMapDirty = true;
private _shadowCameraDirty = true;
private _shadowCameraInverseWorldScale: float = 0;
private _shadowCameraHelper: THREE.CameraHelper | null;
constructor() {
@@ -46,18 +47,28 @@ namespace gdjs {
this._light.shadow.camera.updateProjectionMatrix();
}
private _updateShadowCamera(): void {
if (!this._shadowCameraDirty) {
private _updateShadowCamera(scene: gdjs.RuntimeScene): void {
const inverseWorldScale = scene.getRenderer3DInverseWorldScale();
// The world scale can be changed in the editor.
if (
!this._shadowCameraDirty &&
this._shadowCameraInverseWorldScale === inverseWorldScale
) {
return;
}
this._shadowCameraDirty = false;
this._shadowCameraInverseWorldScale = inverseWorldScale;
const frustumSize = this._frustumSize * inverseWorldScale;
this._light.shadow.camera.near = 1;
this._light.shadow.camera.far = this._distanceFromCamera + 10000;
this._light.shadow.camera.right = this._frustumSize / 2;
this._light.shadow.camera.left = -this._frustumSize / 2;
this._light.shadow.camera.top = this._frustumSize / 2;
this._light.shadow.camera.bottom = -this._frustumSize / 2;
this._light.shadow.camera.near = 1 * inverseWorldScale;
this._light.shadow.camera.far =
(this._distanceFromCamera + 10000) * inverseWorldScale;
this._light.shadow.camera.right = frustumSize / 2;
this._light.shadow.camera.left = -frustumSize / 2;
this._light.shadow.camera.top = frustumSize / 2;
this._light.shadow.camera.bottom = -frustumSize / 2;
// Three.js only updates it when the shadow map is created.
this._light.shadow.camera.updateProjectionMatrix();
}
private _updateShadowMapSize(): void {
@@ -124,8 +135,11 @@ namespace gdjs {
return true;
}
updatePreRender(target: gdjs.EffectsTarget): any {
const scene = target.getRuntimeScene().getScene();
const inverseWorldScale = scene.getRenderer3DInverseWorldScale();
// Apply any update to the camera or shadow map size.
this._updateShadowCamera();
this._updateShadowCamera(scene);
this._updateShadowMapSize();
// Avoid shadow acne due to depth buffer precision.
@@ -135,7 +149,8 @@ namespace gdjs {
: this._shadowMapSize < 2048
? 1.25
: 1;
this._light.shadow.bias = -this._minimumShadowBias * biasMultiplier;
this._light.shadow.bias =
-this._minimumShadowBias * biasMultiplier * inverseWorldScale;
// Apply update to the light position and its target.
// By doing this, the shadows are "following" the GDevelop camera.
@@ -147,6 +162,8 @@ namespace gdjs {
const y = layer.getCameraY();
const z = layer.getCameraZ(layer.getInitialCamera3DFieldOfView());
// Contrary to cameras, lights are within the scene so they already
// take world scale into account.
const roundedX = Math.floor(x / 100) * 100;
const roundedY = Math.floor(y / 100) * 100;
const roundedZ = Math.floor(z / 100) * 100;
@@ -182,14 +199,13 @@ namespace gdjs {
roundedZ +
this._distanceFromCamera *
Math.sin(gdjs.toRad(this._elevation));
this._light.position.set(posLightX, posLightY, posLightZ);
this._light.target.position.set(roundedX, roundedY, roundedZ);
}
}
updateDoubleParameter(parameterName: string, value: number): void {
if (parameterName === 'intensity') {
this._light.intensity = value;
this._light.intensity = value * Math.PI;
} else if (parameterName === 'elevation') {
this._elevation = value;
} else if (parameterName === 'rotation') {
@@ -204,7 +220,7 @@ namespace gdjs {
}
getDoubleParameter(parameterName: string): number {
if (parameterName === 'intensity') {
return this._light.intensity;
return this._light.intensity / Math.PI;
} else if (parameterName === 'elevation') {
return this._elevation;
} else if (parameterName === 'rotation') {
+17 -5
View File
@@ -15,6 +15,9 @@ namespace gdjs {
}
return new (class implements gdjs.PixiFiltersTools.Filter {
fog: THREE.FogExp2;
// In scene units: the fog is converted to the Three.js world units
// before each rendering, as the world scale can change in the editor.
private _density: float = 0.0012;
constructor() {
this.fog = new THREE.FogExp2(0xffffff);
@@ -56,15 +59,23 @@ namespace gdjs {
scene.fog = null;
return true;
}
updatePreRender(target: gdjs.EffectsTarget): any {}
updatePreRender(target: gdjs.EffectsTarget): any {
this._updateFogDensity();
}
private _updateFogDensity(): void {
const scene = target.getRuntimeScene().getScene();
const worldScale = scene.getRenderer3DWorldScale();
this.fog.density = this._density * worldScale;
}
updateDoubleParameter(parameterName: string, value: number): void {
if (parameterName === 'density') {
this.fog.density = value;
this._density = value;
this._updateFogDensity();
}
}
getDoubleParameter(parameterName: string): number {
if (parameterName === 'density') {
return this.fog.density;
return this._density;
}
return 0;
}
@@ -89,14 +100,15 @@ namespace gdjs {
updateBooleanParameter(parameterName: string, value: boolean): void {}
getNetworkSyncData(): ExponentialFogFilterNetworkSyncData {
return {
d: this.fog.density,
d: this._density,
c: this.fog.color.getHex(),
};
}
updateFromNetworkSyncData(
syncData: ExponentialFogFilterNetworkSyncData
): void {
this.fog.density = syncData.d;
this._density = syncData.d;
this._updateFogDensity();
this.fog.color.setHex(syncData.c);
}
})();
+2 -2
View File
@@ -70,7 +70,7 @@ namespace gdjs {
updatePreRender(target: gdjs.EffectsTarget): any {}
updateDoubleParameter(parameterName: string, value: number): void {
if (parameterName === 'intensity') {
this._light.intensity = value;
this._light.intensity = value * Math.PI;
} else if (parameterName === 'elevation') {
this._elevation = value;
this.updateRotation();
@@ -81,7 +81,7 @@ namespace gdjs {
}
getDoubleParameter(parameterName: string): number {
if (parameterName === 'intensity') {
return this._light.intensity;
return this._light.intensity / Math.PI;
} else if (parameterName === 'elevation') {
return this._elevation;
} else if (parameterName === 'rotation') {
+23 -9
View File
@@ -16,6 +16,10 @@ namespace gdjs {
}
return new (class implements gdjs.PixiFiltersTools.Filter {
fog: THREE.Fog;
// In scene units: the fog is converted to the Three.js world units
// before each rendering, as the world scale can change in the editor.
private _near: float = 200;
private _far: float = 2000;
constructor() {
this.fog = new THREE.Fog(0xffffff);
@@ -57,19 +61,28 @@ namespace gdjs {
scene.fog = null;
return true;
}
updatePreRender(target: gdjs.EffectsTarget): any {}
updatePreRender(target: gdjs.EffectsTarget): any {
this._updateFogDistances();
}
private _updateFogDistances(): void {
const scene = target.getRuntimeScene().getScene();
const inverseWorldScale = scene.getRenderer3DInverseWorldScale();
this.fog.near = this._near * inverseWorldScale;
this.fog.far = this._far * inverseWorldScale;
}
updateDoubleParameter(parameterName: string, value: number): void {
if (parameterName === 'near') {
this.fog.near = value;
this._near = value;
} else if (parameterName === 'far') {
this.fog.far = value;
this._far = value;
}
this._updateFogDistances();
}
getDoubleParameter(parameterName: string): number {
if (parameterName === 'near') {
return this.fog.near;
return this._near;
} else if (parameterName === 'far') {
return this.fog.far;
return this._far;
}
return 0;
}
@@ -94,16 +107,17 @@ namespace gdjs {
updateBooleanParameter(parameterName: string, value: boolean): void {}
getNetworkSyncData(): LinearFogFilterNetworkSyncData {
return {
n: this.fog.near,
f: this.fog.far,
n: this._near,
f: this._far,
c: this.fog.color.getHex(),
};
}
updateFromNetworkSyncData(
data: LinearFogFilterNetworkSyncData
): void {
this.fog.near = data.n;
this.fog.far = data.f;
this._near = data.n;
this._far = data.f;
this._updateFogDistances();
this.fog.color.setHex(data.c);
}
})();
+10 -4
View File
@@ -178,11 +178,12 @@ namespace gdjs {
} else {
threeCamera.up.set(0, 0, 1);
}
const inverseWorldScale = runtimeScene.getRenderer3DInverseWorldScale();
threeCamera.lookAt(
object.getCenterXInScene(),
-object.getCenterYInScene(),
object.getCenterXInScene() * inverseWorldScale,
-object.getCenterYInScene() * inverseWorldScale,
//@ts-ignore
object.getZ ? object.getZ() : 0
object.getZ ? object.getZ() * inverseWorldScale : 0
);
// The layer angle takes over the 3D camera Z rotation.
layer.setCameraRotation(gdjs.toDegrees(-threeCamera.rotation.z));
@@ -208,7 +209,12 @@ namespace gdjs {
} else {
threeCamera.up.set(0, 0, 1);
}
threeCamera.lookAt(x, -y, z);
const inverseWorldScale = runtimeScene.getRenderer3DInverseWorldScale();
threeCamera.lookAt(
x * inverseWorldScale,
-y * inverseWorldScale,
z * inverseWorldScale
);
// The layer angle takes over the 3D camera Z rotation.
layer.setCameraRotation(gdjs.toDegrees(-threeCamera.rotation.z));
};
@@ -36,6 +36,7 @@ describe('gdjs.AnchorRuntimeBehavior', () => {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
@@ -25,6 +25,7 @@ describe('gdjs.DraggableRuntimeBehavior', function () {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
@@ -35,6 +35,7 @@ describe('gdjs.LinksManager', function () {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
usedResources: [],
uiSettings: {
grid: false,
@@ -35,6 +35,7 @@ describe('gdjs.NavMeshCharacterRuntimeBehavior', function () {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 1,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
@@ -32,6 +32,7 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
@@ -35,6 +35,7 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
@@ -37,6 +37,7 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
@@ -39,6 +39,7 @@ describe('gdjs.PhysicsCharacter3DRuntimeBehavior', () => {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
@@ -45,6 +45,7 @@ describe('gdjs.ShapePainterRuntimeObject (using a PixiJS RuntimeGame with assets
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
@@ -54,6 +54,7 @@ describe('SaveState', () => {
name: name,
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: objects || [
// @ts-ignore - This is a gdjs.SpriteObjectData.
@@ -56,6 +56,7 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
@@ -53,6 +53,7 @@ describe('gdjs.TileMapCollisionMaskRuntimeObject', function () {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
@@ -30,6 +30,7 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
+34 -14
View File
@@ -598,11 +598,11 @@ namespace gdjs {
{
effectType: 'Scene3D::HemisphereLight',
name: 'Default Light for in-game editor',
doubleParameters: { elevation: 45, intensity: 1, rotation: 0 },
doubleParameters: { elevation: 45, intensity: 1, rotation: 90 },
stringParameters: {
groundColor: '64;64;64',
skyColor: '255;255;255',
top: 'Y-',
top: 'Z+',
},
booleanParameters: {},
},
@@ -1675,12 +1675,12 @@ namespace gdjs {
doubleParameters: {
elevation: 45,
intensity: 1,
rotation: 0,
rotation: 90,
},
stringParameters: {
groundColor: '64;64;64',
skyColor: '255;255;255',
top: 'Y-',
top: 'Z+',
},
booleanParameters: {},
},
@@ -2771,10 +2771,15 @@ namespace gdjs {
threeTransformControls
);
threeTransformControls.rotation.order = 'ZYX';
threeTransformControls.scale.y = -1;
threeTransformControls.getHelper().rotation.order = 'ZYX';
const worldScale = this._currentScene
? this._currentScene.getRenderer3DWorldScale()
: 1;
threeTransformControls
.getHelper()
.scale.set(worldScale, -worldScale, worldScale);
threeTransformControls.mode = this._transformControlsMode;
threeTransformControls.traverse((obj) => {
threeTransformControls.getHelper().traverse((obj) => {
// To be detected correctly by OutlinePass.
// @ts-ignore
obj.isTransformControls = true;
@@ -2791,7 +2796,7 @@ namespace gdjs {
threeScene.add(dummyThreeObject);
threeTransformControls.attach(dummyThreeObject);
threeScene.add(threeTransformControls);
threeScene.add(threeTransformControls.getHelper());
// Keep track of the movement so the editor can apply it to the selection.
let initialObjectX = 0;
@@ -2928,6 +2933,8 @@ namespace gdjs {
// Three.js computes the scale as a ratio of the pointer distances
// to the gizmo, which barely changes the size of small objects.
// Use the pointer movement in the object local axes instead.
// The pointer positions are in the Three.js world, so they are
// converted back to scene units with the world scale.
const { pointStart, pointEnd } = threeTransformControls as any;
dummyThreeObject.matrixWorld.decompose(
scaleDragWorldPosition,
@@ -2937,17 +2944,20 @@ namespace gdjs {
scaleDragWorldQuaternion.invert();
scaleDragLocalStart
.copy(pointStart)
.applyQuaternion(scaleDragWorldQuaternion);
.applyQuaternion(scaleDragWorldQuaternion)
.multiplyScalar(worldScale);
scaleDragLocalEnd
.copy(pointEnd)
.applyQuaternion(scaleDragWorldQuaternion);
.applyQuaternion(scaleDragWorldQuaternion)
.multiplyScalar(worldScale);
// Moving away from the gizmo center enlarges the object,
// whichever side of the axis the handle is on.
const getSizeDelta = (start: float, end: float) =>
(end - start) * (start < 0 ? -1 : 1);
const uniformSizeDelta =
(pointEnd as THREE.Vector3).length() -
(pointStart as THREE.Vector3).length();
((pointEnd as THREE.Vector3).length() -
(pointStart as THREE.Vector3).length()) *
worldScale;
const isUniform = threeTransformControls.axis === 'XYZ';
const editorGrid = this._editorGrid;
@@ -3161,7 +3171,9 @@ namespace gdjs {
return;
}
this._selectionControls.threeTransformControls.detach();
this._selectionControls.threeTransformControls.removeFromParent();
this._selectionControls.threeTransformControls
.getHelper()
.removeFromParent();
this._selectionControls.dummyThreeObject.removeFromParent();
this._editorGrid.setVisible(false);
this._selectionControls = null;
@@ -3937,6 +3949,11 @@ namespace gdjs {
const firstIntersect = intersects[0];
if (!firstIntersect) return;
const worldScale = currentScene.getRenderer3DWorldScale();
firstIntersect.distance *= worldScale;
firstIntersect.point.x *= worldScale;
firstIntersect.point.y *= worldScale;
firstIntersect.point.z *= worldScale;
firstIntersectsByLayer[layerName] = {
intersect: firstIntersect,
};
@@ -6136,7 +6153,6 @@ namespace gdjs {
// of the BoxHelper is always (0, 0, 0) and the geometry is hard to manipulate.
this.container = new THREE.Group();
this.container.rotation.order = 'ZYX';
this.container.scale.y = -1;
this.boxHelper = new THREE.BoxHelper(threeObject, '#f2a63c');
this.boxHelper.rotation.order = 'ZYX';
this.boxHelper.material.depthTest = false;
@@ -6145,6 +6161,10 @@ namespace gdjs {
}
update() {
const worldScale = this.object
.getRuntimeScene()
.getRenderer3DWorldScale();
this.container.scale.set(worldScale, -worldScale, worldScale);
if (this.dummyObject3DForObject2D) {
this.dummyObject3DForObject2D.position.set(
this.object.getCenterXInScene(),
+2 -2
View File
@@ -55,7 +55,7 @@ namespace gdjs {
type SharedTextureEntry = {
key: string;
texture: Promise<THREE.Texture>;
source?: THREE.Source;
source?: THREE.Source<unknown>;
};
/**
@@ -66,7 +66,7 @@ namespace gdjs {
class SharedTextureCache {
private _textures = new Map<string, SharedTextureEntry>();
private _sources = new Map<
THREE.Source,
THREE.Source<unknown>,
{ entries: Set<SharedTextureEntry>; users: Set<string> }
>();
private _retainedTextures = new Set<THREE.Texture>();
@@ -406,6 +406,27 @@ namespace gdjs {
}
}
}
} else if (data.command === 'setRenderer3DWorldScale') {
if (inGameEditor) {
const editedInstanceContainer =
inGameEditor.getEditedInstanceContainer();
if (editedInstanceContainer) {
const renderer3DWorldScale = data.payload.renderer3DWorldScale;
if (
renderer3DWorldScale &&
editedInstanceContainer instanceof gdjs.RuntimeScene
) {
const sceneData = runtimeGame.getSceneData(
editedInstanceContainer.getScene().getName()
);
if (sceneData) {
editedInstanceContainer.setRenderer3DWorldScale(
renderer3DWorldScale
);
}
}
}
}
} else if (data.command === 'hotReloadAllInstances') {
if (inGameEditor) {
const editedInstanceContainer =
@@ -830,6 +830,7 @@ namespace gdjs {
newLayoutData.v,
newLayoutData.b
);
runtimeScene.setRenderer3DWorldScale(newLayoutData.renderer3DWorldScale);
if (oldLayoutData.title !== newLayoutData.title) {
runtimeScene
.getGame()
File diff suppressed because one or more lines are too long
@@ -401,11 +401,6 @@ namespace gdjs {
}
this._threeScene = new THREE.Scene();
// Use a mirroring on the Y axis to follow the same axis as in the 2D, PixiJS, rendering.
// We use a mirroring rather than a camera rotation so that the Z order is not changed.
this._threeScene.scale.y = -1;
this._threeGroup = new THREE.Group();
this._threeScene.add(this._threeGroup);
@@ -419,20 +414,18 @@ namespace gdjs {
-width / 2,
width / 2,
height / 2,
-height / 2,
this._layer.getInitialCamera3DNearPlaneDistance(),
this._layer.getInitialCamera3DFarPlaneDistance()
-height / 2
);
} else {
this._threeCamera = new THREE.PerspectiveCamera(
this._layer.getInitialCamera3DFieldOfView(),
1,
this._layer.getInitialCamera3DNearPlaneDistance(),
this._layer.getInitialCamera3DFarPlaneDistance()
1
);
}
this._threeCamera.rotation.order = 'ZYX';
this.updateWorldScale();
const game = this._layer.getRuntimeScene().getGame();
const threeRenderer = game.getRenderer().getThreeRenderer();
if (threeRenderer) {
@@ -445,12 +438,7 @@ namespace gdjs {
new THREE_ADDONS.RenderPass(this._threeScene, this._threeCamera)
);
if (game.getAntialiasingMode() !== 'none') {
this._threeEffectComposer.addPass(
new THREE_ADDONS.SMAAPass(
game.getGameResolutionWidth(),
game.getGameResolutionHeight()
)
);
this._threeEffectComposer.addPass(new THREE_ADDONS.SMAAPass());
}
this._threeEffectComposer.addPass(new THREE_ADDONS.OutputPass());
}
@@ -569,10 +557,13 @@ namespace gdjs {
setCamera3DNearPlaneDistance(distance: number) {
if (!this._threeCamera) return;
const inverseWorldScale = this._layer
.getRuntimeScene()
.getRenderer3DInverseWorldScale();
this._threeCamera.near = Math.min(
// 0 is not a valid value for three js perspective camera:
// https://threejs.org/docs/#api/en/cameras/PerspectiveCamera.
Math.max(distance, 0.0001),
Math.max(distance * inverseWorldScale, 0.000001),
// Near value cannot exceed far value.
this._threeCamera.far
);
@@ -581,18 +572,30 @@ namespace gdjs {
getCamera3DNearPlaneDistance(): float {
if (!this._threeCamera) return 0;
return this._threeCamera.near;
const worldScale = this._layer
.getRuntimeScene()
.getRenderer3DWorldScale();
return this._threeCamera.near * worldScale;
}
setCamera3DFarPlaneDistance(distance: number) {
if (!this._threeCamera) return;
this._threeCamera.far = Math.max(distance, this._threeCamera.near);
const inverseWorldScale = this._layer
.getRuntimeScene()
.getRenderer3DInverseWorldScale();
this._threeCamera.far = Math.max(
distance * inverseWorldScale,
this._threeCamera.near
);
this._threeCameraDirty = true;
}
getCamera3DFarPlaneDistance(): float {
if (!this._threeCamera) return 0;
return this._threeCamera.far;
const worldScale = this._layer
.getRuntimeScene()
.getRenderer3DWorldScale();
return this._threeCamera.far * worldScale;
}
setCamera3DFieldOfView(angle: number) {
@@ -828,25 +831,52 @@ namespace gdjs {
return [cx, cy];
}
updateWorldScale(): void {
if (!this._threeScene || !this._threeCamera) {
return;
}
const inverseWorldScale = this._layer
.getRuntimeScene()
.getRenderer3DInverseWorldScale();
// Use a mirroring on the Y axis to follow the same axis as in the 2D, PixiJS, rendering.
// We use a mirroring rather than a camera rotation so that the Z order is not changed.
this._threeScene.scale.set(
inverseWorldScale,
-inverseWorldScale,
inverseWorldScale
);
this._threeCamera.near =
this._layer.getInitialCamera3DNearPlaneDistance() * inverseWorldScale;
this._threeCamera.far =
this._layer.getInitialCamera3DFarPlaneDistance() * inverseWorldScale;
}
updatePosition(): void {
const instanceContainer = this._layer.getInstanceContainer();
const runtimeGame = instanceContainer.getGame();
const inverseWorldScale = this._layer
.getRuntimeScene()
.getRenderer3DInverseWorldScale();
// Update the 3D camera position and rotation.
if (this._threeCamera) {
const angle = -gdjs.toRad(this._layer.getCameraRotation());
this._threeCamera.position.x = this._layer.getCameraX();
this._threeCamera.position.y = -this._layer.getCameraY(); // scene is mirrored on Y
this._threeCamera.position.x =
this._layer.getCameraX() * inverseWorldScale;
// The scene is mirrored on Y
this._threeCamera.position.y =
-this._layer.getCameraY() * inverseWorldScale;
this._threeCamera.rotation.z = angle;
if (this._threeCamera instanceof THREE.OrthographicCamera) {
this._threeCamera.zoom = this._layer.getCameraZoom();
this._threeCamera.updateProjectionMatrix();
this._threeCamera.position.z = this._layer.getCameraZ(null);
this._threeCamera.position.z =
this._layer.getCameraZ(null) * inverseWorldScale;
} else {
this._threeCamera.position.z = this._layer.getCameraZ(
this._threeCamera.fov
);
this._threeCamera.position.z =
this._layer.getCameraZ(this._threeCamera.fov) * inverseWorldScale;
}
}
@@ -994,7 +1024,6 @@ namespace gdjs {
updateResolution() {
if (this._threeEffectComposer) {
const game = this._layer.getRuntimeScene().getGame();
this._threeEffectComposer.setPixelRatio(window.devicePixelRatio);
this._threeEffectComposer.setSize(
game.getGameResolutionWidth(),
game.getGameResolutionHeight()
@@ -1336,7 +1365,9 @@ namespace gdjs {
// "Hack" into the Three.js renderer by getting the internal WebGL texture for the PixiJS plane,
// and set it so that it's the same as the WebGL texture for the PixiJS RenderTexture.
// This works because PixiJS and Three.js are using the same WebGL context.
const texture = threeRenderer.properties.get(this._threePlaneTexture);
const texture: any = threeRenderer.properties.get(
this._threePlaneTexture
);
texture.__webglTexture = glTexture.texture;
}
}
@@ -55,7 +55,7 @@ namespace gdjs {
/**
* Map associating a resource name to the loaded Three.js texture.
*/
private _loadedThreeTextures: Hashtable<THREE.Texture>;
private _loadedThreeTextures: Hashtable<THREE.Texture<HTMLImageElement>>;
private _loadedThreeMaterials = new ThreeMaterialCache();
private _loadedThreeCubeTextures = new Map<string, THREE.CubeTexture>();
private _loadedThreeCubeTextureKeysByResourceName = new ArrayMap<
@@ -186,7 +186,7 @@ namespace gdjs {
* @param resourceName The name of the resource
* @returns The requested texture, or a placeholder if not found.
*/
getThreeTexture(resourceName: string): THREE.Texture {
getThreeTexture(resourceName: string): THREE.Texture<HTMLImageElement> {
const loadedThreeTexture = this._loadedThreeTextures.get(resourceName);
if (loadedThreeTexture) {
return loadedThreeTexture;
@@ -567,7 +567,7 @@ namespace gdjs {
dispose(): void {
this._loadedTextures.clear();
const threeTextures: THREE.Texture[] = [];
const threeTextures: Array<THREE.Texture<HTMLImageElement>> = [];
this._loadedThreeTextures.values(threeTextures);
this._loadedThreeTextures.clear();
for (const threeTexture of threeTextures) {
@@ -114,10 +114,10 @@ namespace gdjs {
preserveDrawingBuffer: true, // Keep to true to allow screenshots.
});
this._threeRenderer.shadowMap.enabled = true;
this._threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap;
this._threeRenderer.useLegacyLights = true;
// PCFSoftShadowMap is deprecated since Three.js r180 and replaced by
// PCFShadowMap (with a warning) when rendering.
this._threeRenderer.shadowMap.type = THREE.PCFShadowMap;
this._threeRenderer.autoClear = false;
this._threeRenderer.pixelRatio = window.devicePixelRatio;
this._threeRenderer.setSize(
this._game.getGameResolutionWidth(),
this._game.getGameResolutionHeight()
File diff suppressed because one or more lines are too long
+24
View File
@@ -61,6 +61,9 @@ namespace gdjs {
*/
networkId: string | null = null;
private _renderer3DWorldScale: float = 100;
private _renderer3DInverseWorldScale: float = 0.01;
/**
* @param runtimeGame The game associated to this scene.
*/
@@ -156,6 +159,7 @@ namespace gdjs {
}
this._name = sceneData.name;
this._resourcesUnloading = sceneData.resourcesUnloading || 'inherit';
this.setRenderer3DWorldScale(sceneData.renderer3DWorldScale);
this.setBackgroundColor(sceneData.r, sceneData.v, sceneData.b);
//Load layers
@@ -964,6 +968,26 @@ namespace gdjs {
}
return this.networkId;
}
setRenderer3DWorldScale(worldScale: float): void {
const value = Math.abs(worldScale) || 100;
this._renderer3DWorldScale = value;
this._renderer3DInverseWorldScale = 1 / value;
for (const layer of this._orderedLayers) {
const layerRenderer = layer.getRenderer();
layerRenderer.updateWorldScale();
layerRenderer.updatePosition();
}
}
getRenderer3DWorldScale(): float {
return this._renderer3DWorldScale;
}
getRenderer3DInverseWorldScale(): float {
return this._renderer3DInverseWorldScale;
}
}
/**
+1
View File
@@ -311,6 +311,7 @@ declare interface LayoutData extends InstanceContainerData {
usedResources: ResourceReference[];
resourcesPreloading?: 'at-startup' | 'never' | 'inherit';
resourcesUnloading?: 'at-scene-exit' | 'never' | 'inherit';
renderer3DWorldScale: float;
uiSettings: InstancesEditorSettings;
}
+30 -13
View File
@@ -15,7 +15,7 @@
"@types/mocha": "^5.2.7",
"@types/node": "^14.11.1",
"@types/sinon": "^10.0.13",
"@types/three": "0.160.0",
"@types/three": "0.185.1",
"esbuild": "^0.13.12",
"lebab": "^3.1.0",
"minimist": "^1.2.5",
@@ -30,6 +30,13 @@
"typescript": "5.4.5"
}
},
"node_modules/@dimforge/rapier3d-compat": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@esotericsoftware/spine-core": {
"version": "4.2.116",
"resolved": "https://registry.npmjs.org/@esotericsoftware/spine-core/-/spine-core-4.2.116.tgz",
@@ -314,6 +321,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/css-font-loading-module": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz",
@@ -374,16 +388,18 @@
"license": "MIT"
},
"node_modules/@types/three": {
"version": "0.160.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.160.0.tgz",
"integrity": "sha512-jWlbUBovicUKaOYxzgkLlhkiEQJkhCVvg4W2IYD2trqD2om3VK4DGLpHH5zQHNr7RweZK/5re/4IVhbhvxbV9w==",
"version": "0.185.1",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.1.tgz",
"integrity": "sha512-db1xTb+EgYF2didW+eudSvVPtn75zo+fGsY8ShQrJY/B5ZBmC2Fiaykv3aImHAlCNEGuMPkPGXBJGLwzu5mC7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3",
"@types/stats.js": "*",
"@types/webxr": "*",
"fflate": "~0.6.10",
"meshoptimizer": "~0.18.1"
"@types/webxr": ">=0.5.17",
"fflate": "~0.8.2",
"meshoptimizer": "~1.1.1"
}
},
"node_modules/@types/webxr": {
@@ -406,6 +422,7 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1069,9 +1086,9 @@
}
},
"node_modules/fflate": {
"version": "0.6.10",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"dev": true,
"license": "MIT"
},
@@ -1500,9 +1517,9 @@
}
},
"node_modules/meshoptimizer": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
"integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
"dev": true,
"license": "MIT"
},
+1 -1
View File
@@ -10,7 +10,7 @@
"@types/mocha": "^5.2.7",
"@types/node": "^14.11.1",
"@types/sinon": "^10.0.13",
"@types/three": "0.160.0",
"@types/three": "0.185.1",
"esbuild": "^0.13.12",
"lebab": "^3.1.0",
"minimist": "^1.2.5",
+34
View File
@@ -0,0 +1,34 @@
diff --git a/node_modules/@types/three/build/three.d.cts b/node_modules/@types/three/build/three.d.cts
index 1e831f0..e3f7ddd 100644
--- a/node_modules/@types/three/build/three.d.cts
+++ b/node_modules/@types/three/build/three.d.cts
@@ -1 +1,2 @@
export * from "../src/Three.js";
+export as namespace THREE;
\ No newline at end of file
diff --git a/node_modules/@types/three/build/three.module.d.ts b/node_modules/@types/three/build/three.module.d.ts
index 1e831f0..e3f7ddd 100644
--- a/node_modules/@types/three/build/three.module.d.ts
+++ b/node_modules/@types/three/build/three.module.d.ts
@@ -1 +1,2 @@
export * from "../src/Three.js";
+export as namespace THREE;
\ No newline at end of file
diff --git a/node_modules/@types/three/build/three.module.min.d.ts b/node_modules/@types/three/build/three.module.min.d.ts
index 1e831f0..e3f7ddd 100644
--- a/node_modules/@types/three/build/three.module.min.d.ts
+++ b/node_modules/@types/three/build/three.module.min.d.ts
@@ -1 +1,2 @@
export * from "../src/Three.js";
+export as namespace THREE;
\ No newline at end of file
diff --git a/node_modules/@types/three/index.d.ts b/node_modules/@types/three/index.d.ts
index 7e839d2..9253d1e 100644
--- a/node_modules/@types/three/index.d.ts
+++ b/node_modules/@types/three/index.d.ts
@@ -4,3 +4,4 @@
// and released in the @types/three npm package.
export * from "./src/Three.js";
+export as namespace THREE;
\ No newline at end of file
@@ -97,6 +97,7 @@ gdjs.getPixiRuntimeGameWithAssets = (props = null) => {
behaviorsSharedData: [],
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
variables: [],
usedResources: [],
uiSettings: {
+1
View File
@@ -38,6 +38,7 @@ describe('gdjs.ResourceLoader', () => {
behaviorsSharedData: [],
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
variables: [],
usedResources,
uiSettings: {
+1 -1
View File
@@ -87,7 +87,6 @@ describe('gdjs.EffectsManager', () => {
},
],
cameras: [],
ambientLightColorR: 0,
ambientLightColorG: 0,
ambientLightColorB: 0,
@@ -103,6 +102,7 @@ describe('gdjs.EffectsManager', () => {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
+1
View File
@@ -178,6 +178,7 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: objects || [
createSpriteData({ name: 'MyObject', image: 'ResourceA' }),
+1
View File
@@ -30,6 +30,7 @@ describe('gdjs.RuntimeScene integration tests', function () {
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [
{
+1
View File
@@ -20,6 +20,7 @@ describe('gdjs.SceneStack', () => {
behaviorsSharedData: [],
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
variables: [],
usedResources,
uiSettings: {
+3
View File
@@ -1069,6 +1069,9 @@ interface Layout {
[Const, Ref] DOMString GetResourcesPreloading();
void SetResourcesUnloading([Const] DOMString resourcesUnloading);
[Const, Ref] DOMString GetResourcesUnloading();
void SetRenderer3DWorldScale(double value);
double GetRenderer3DWorldScale();
};
interface ExternalEvents {
+2
View File
@@ -901,6 +901,8 @@ export class Layout extends EmscriptenObject {
getResourcesPreloading(): string;
setResourcesUnloading(resourcesUnloading: string): void;
getResourcesUnloading(): string;
setRenderer3DWorldScale(value: number): void;
getRenderer3DWorldScale(): number;
}
export class ExternalEvents extends EmscriptenObject {
+2
View File
@@ -36,6 +36,8 @@ declare class gdLayout {
getResourcesPreloading(): string;
setResourcesUnloading(resourcesUnloading: string): void;
getResourcesUnloading(): string;
setRenderer3DWorldScale(value: number): void;
getRenderer3DWorldScale(): number;
delete(): void;
ptr: number;
};
+1 -1
View File
@@ -17,7 +17,7 @@
"dependencies": {
},
"devDependencies": {
"three": "0.160.0",
"three": "0.185.1",
"@rollup/plugin-node-resolve": "^13.1.3",
"rollup": "^2.66.1",
"@rollup/plugin-terser": "^0.4.0",
+12 -2
View File
@@ -13,13 +13,13 @@ export default [
output: [
{
name: 'THREE',
format: 'iife',
format: 'umd',
file: './dist/three.js',
sourcemap: true,
},
{
name: 'THREE',
format: 'iife',
format: 'umd',
file: '../../GDJS/Runtime/pixi-renderers/three.js',
sourcemap: false,
},
@@ -44,6 +44,16 @@ export default [
'three': 'THREE',
},
},
{
name: 'THREE_ADDONS',
format: 'umd',
file: '../../GDJS/Runtime/pixi-renderers/ThreeAddons.js',
sourcemap: false,
plugins: [],
globals: {
'three': 'THREE',
},
},
],
external: ['three'],
plugins: [
@@ -1,6 +1,7 @@
import {
BoxGeometry,
BufferGeometry,
Controls,
CylinderGeometry,
DoubleSide,
Euler,
@@ -31,37 +32,70 @@ const _unit = {
Z: new Vector3( 0, 0, 1 )
};
/**
* Fires if any type of change (object or property change) is performed. Property changes
* are separate events you can add event listeners to. The event type is "propertyname-changed".
*
* @event TransformControls#change
* @type {Object}
*/
const _changeEvent = { type: 'change' };
const _mouseDownEvent = { type: 'mouseDown' };
/**
* Fires if a pointer (mouse/touch) becomes active.
*
* @event TransformControls#mouseDown
* @type {Object}
*/
const _mouseDownEvent = { type: 'mouseDown', mode: null };
/**
* Fires if a pointer (mouse/touch) is no longer active.
*
* @event TransformControls#mouseUp
* @type {Object}
*/
const _mouseUpEvent = { type: 'mouseUp', mode: null };
/**
* Fires if the controlled 3D object is changed.
*
* @event TransformControls#objectChange
* @type {Object}
*/
const _objectChangeEvent = { type: 'objectChange' };
class TransformControls extends Object3D {
/**
* This class can be used to transform objects in 3D space by adapting a similar interaction model
* of DCC tools like Blender. Unlike other controls, it is not intended to transform the scene's camera.
*
* `TransformControls` expects that its attached 3D object is part of the scene graph.
*
* @augments Controls
* @three_import import { TransformControls } from 'three/addons/controls/TransformControls.js';
*/
class TransformControls extends Controls {
constructor( camera, domElement ) {
/**
* Constructs a new controls instance.
*
* @param {Camera} camera - The camera of the rendered scene.
* @param {?HTMLElement} domElement - The HTML element used for event listeners.
*/
constructor( camera, domElement = null ) {
super();
super( undefined, domElement );
if ( domElement === undefined ) {
const root = new TransformControlsRoot( this );
this._root = root;
console.warn( 'THREE.TransformControls: The second parameter "domElement" is now mandatory.' );
domElement = document;
const gizmo = new TransformControlsGizmo();
this._gizmo = gizmo;
root.add( gizmo );
}
this.isTransformControls = true;
this.visible = false;
this.domElement = domElement;
this.domElement.style.touchAction = 'none'; // disable touch scroll
const _gizmo = new TransformControlsGizmo();
this._gizmo = _gizmo;
this.add( _gizmo );
const _plane = new TransformControlsPlane();
this._plane = _plane;
this.add( _plane );
const plane = new TransformControlsPlane();
this._plane = plane;
root.add( plane );
const scope = this;
@@ -83,8 +117,8 @@ class TransformControls extends Object3D {
if ( propValue !== value ) {
propValue = value;
_plane[ propName ] = value;
_gizmo[ propName ] = value;
plane[ propName ] = value;
gizmo[ propName ] = value;
scope.dispatchEvent( { type: propName + '-changed', value: value } );
scope.dispatchEvent( _changeEvent );
@@ -96,8 +130,8 @@ class TransformControls extends Object3D {
} );
scope[ propName ] = defaultValue;
_plane[ propName ] = defaultValue;
_gizmo[ propName ] = defaultValue;
plane[ propName ] = defaultValue;
gizmo[ propName ] = defaultValue;
}
@@ -105,21 +139,228 @@ class TransformControls extends Object3D {
// Setting the defined property will automatically trigger change event
// Defined properties are passed down to gizmo and plane
/**
* The camera of the rendered scene.
*
* @name TransformControls#camera
* @type {Camera}
*/
defineProperty( 'camera', camera );
defineProperty( 'object', undefined );
defineProperty( 'enabled', true );
/**
* The current transformation axis.
*
* @name TransformControls#axis
* @type {string}
*/
defineProperty( 'axis', null );
/**
* The current transformation axis.
*
* @name TransformControls#mode
* @type {('translate'|'rotate'|'scale')}
* @default 'translate'
*/
defineProperty( 'mode', 'translate' );
/**
* By default, 3D objects are continuously translated. If you set this property to a numeric
* value (world units), you can define in which steps the 3D object should be translated.
*
* @name TransformControls#translationSnap
* @type {?number}
* @default null
*/
defineProperty( 'translationSnap', null );
/**
* By default, 3D objects are continuously rotated. If you set this property to a numeric
* value (radians), you can define in which steps the 3D object should be rotated.
*
* @name TransformControls#rotationSnap
* @type {?number}
* @default null
*/
defineProperty( 'rotationSnap', null );
/**
* By default, 3D objects are continuously scaled. If you set this property to a numeric
* value, you can define in which steps the 3D object should be scaled.
*
* @name TransformControls#scaleSnap
* @type {?number}
* @default null
*/
defineProperty( 'scaleSnap', null );
/**
* Defines in which coordinate space transformations should be performed.
*
* @name TransformControls#space
* @type {('world'|'local')}
* @default 'world'
*/
defineProperty( 'space', 'world' );
/**
* The size of the helper UI (axes/planes).
*
* @name TransformControls#size
* @type {number}
* @default 1
*/
defineProperty( 'size', 1 );
/**
* The viewport rectangle, in logical (CSS) pixels with the origin at the lower-left
* of the canvas. Set this when the renderer uses a sub-canvas viewport so pointer
* coordinates map to the correct region. If `null`, the full canvas is used.
*
* @name TransformControls#viewport
* @type {?Vector4}
* @default null
*/
this.viewport = null;
/**
* Whether dragging is currently performed or not.
*
* @name TransformControls#dragging
* @type {boolean}
* @readonly
* @default false
*/
defineProperty( 'dragging', false );
/**
* Whether the x-axis helper should be visible or not.
*
* @name TransformControls#showX
* @type {boolean}
* @default true
*/
defineProperty( 'showX', true );
/**
* Whether the y-axis helper should be visible or not.
*
* @name TransformControls#showY
* @type {boolean}
* @default true
*/
defineProperty( 'showY', true );
/**
* Whether the z-axis helper should be visible or not.
*
* @name TransformControls#showZ
* @type {boolean}
* @default true
*/
defineProperty( 'showZ', true );
/**
* Whether the xy-plane helper should be visible or not.
*
* @name TransformControls#showXY
* @type {boolean}
* @default true
*/
defineProperty( 'showXY', true );
/**
* Whether the yz-plane helper should be visible or not.
*
* @name TransformControls#showYZ
* @type {boolean}
* @default true
*/
defineProperty( 'showYZ', true );
/**
* Whether the xz-axis helper should be visible or not.
*
* @name TransformControls#showXZ
* @type {boolean}
* @default true
*/
defineProperty( 'showXZ', true );
/**
* Whether the xyze rotation helper should be visible or not.
*
* @name TransformControls#showXYZE
* @type {boolean}
* @default true
*/
defineProperty( 'showXYZE', true );
/**
* Whether the e rotation helper should be visible or not.
*
* @name TransformControls#showE
* @type {boolean}
* @default true
*/
defineProperty( 'showE', true );
/**
* The minimum allowed X position during translation.
*
* @name TransformControls#minX
* @type {number}
* @default -Infinity
*/
defineProperty( 'minX', - Infinity );
/**
* The maximum allowed X position during translation.
*
* @name TransformControls#maxX
* @type {number}
* @default Infinity
*/
defineProperty( 'maxX', Infinity );
/**
* The minimum allowed y position during translation.
*
* @name TransformControls#minY
* @type {number}
* @default -Infinity
*/
defineProperty( 'minY', - Infinity );
/**
* The maximum allowed Y position during translation.
*
* @name TransformControls#maxY
* @type {number}
* @default Infinity
*/
defineProperty( 'maxY', Infinity );
/**
* The minimum allowed z position during translation.
*
* @name TransformControls#minZ
* @type {number}
* @default -Infinity
*/
defineProperty( 'minZ', - Infinity );
/**
* The maximum allowed Z position during translation.
*
* @name TransformControls#maxZ
* @type {number}
* @default Infinity
*/
defineProperty( 'maxZ', Infinity );
// Reusable utility variables
const worldPosition = new Vector3();
@@ -172,50 +413,46 @@ class TransformControls extends Object3D {
this._onPointerMove = onPointerMove.bind( this );
this._onPointerUp = onPointerUp.bind( this );
if ( domElement !== null ) {
this.connect( domElement );
}
}
connect( element ) {
super.connect( element );
this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
this.domElement.addEventListener( 'pointermove', this._onPointerHover );
this.domElement.addEventListener( 'pointerup', this._onPointerUp );
this.domElement.style.touchAction = 'none'; // Disable touch scroll
}
// updateMatrixWorld updates key transformation variables
updateMatrixWorld() {
disconnect() {
if ( this.object !== undefined ) {
this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
this.domElement.removeEventListener( 'pointermove', this._onPointerHover );
this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
this.object.updateMatrixWorld();
this.domElement.style.touchAction = ''; // Restore touch scroll
if ( this.object.parent === null ) {
}
console.error( 'TransformControls: The attached 3D object must be a part of the scene graph.' );
/**
* Returns the visual representation of the controls. Add the helper to your scene to
* visually transform the attached 3D object.
*
* @return {TransformControlsRoot} The helper.
*/
getHelper() {
} else {
this.object.parent.matrixWorld.decompose( this._parentPosition, this._parentQuaternion, this._parentScale );
}
this.object.matrixWorld.decompose( this.worldPosition, this.worldQuaternion, this._worldScale );
this._parentQuaternionInv.copy( this._parentQuaternion ).invert();
this._worldQuaternionInv.copy( this.worldQuaternion ).invert();
}
this.camera.updateMatrixWorld();
this.camera.matrixWorld.decompose( this.cameraPosition, this.cameraQuaternion, this._cameraScale );
if ( this.camera.isOrthographicCamera ) {
this.camera.getWorldDirection( this.eye ).negate();
} else {
this.eye.copy( this.cameraPosition ).sub( this.worldPosition ).normalize();
}
super.updateMatrixWorld( this );
return this._root;
}
@@ -223,7 +460,7 @@ class TransformControls extends Object3D {
if ( this.object === undefined || this.dragging === true ) return;
_raycaster.setFromCamera( pointer, this.camera );
if ( pointer !== null ) _raycaster.setFromCamera( pointer, this.camera );
const intersect = intersectObjectWithRay( this._gizmo.picker[ this.mode ], _raycaster );
@@ -241,11 +478,11 @@ class TransformControls extends Object3D {
pointerDown( pointer ) {
if ( this.object === undefined || this.dragging === true || pointer.button !== 0 ) return;
if ( this.object === undefined || this.dragging === true || ( pointer != null && pointer.button !== 0 ) ) return;
if ( this.axis !== null ) {
_raycaster.setFromCamera( pointer, this.camera );
if ( pointer !== null ) _raycaster.setFromCamera( pointer, this.camera );
const planeIntersect = intersectObjectWithRay( this._plane, _raycaster, true );
@@ -289,9 +526,9 @@ class TransformControls extends Object3D {
}
if ( object === undefined || axis === null || this.dragging === false || pointer.button !== - 1 ) return;
if ( object === undefined || axis === null || this.dragging === false || ( pointer !== null && pointer.button !== - 1 ) ) return;
_raycaster.setFromCamera( pointer, this.camera );
if ( pointer !== null ) _raycaster.setFromCamera( pointer, this.camera );
const planeIntersect = intersectObjectWithRay( this._plane, _raycaster, true );
@@ -359,40 +596,36 @@ class TransformControls extends Object3D {
if ( space === 'world' ) {
if ( object.parent ) {
object.position.add( _tempVector.setFromMatrixPosition( object.parent.matrixWorld ) );
}
object.getWorldPosition( _tempVector );
if ( axis.search( 'X' ) !== - 1 ) {
object.position.x = Math.round( object.position.x / this.translationSnap ) * this.translationSnap;
_tempVector.x = Math.round( _tempVector.x / this.translationSnap ) * this.translationSnap;
}
if ( axis.search( 'Y' ) !== - 1 ) {
object.position.y = Math.round( object.position.y / this.translationSnap ) * this.translationSnap;
_tempVector.y = Math.round( _tempVector.y / this.translationSnap ) * this.translationSnap;
}
if ( axis.search( 'Z' ) !== - 1 ) {
object.position.z = Math.round( object.position.z / this.translationSnap ) * this.translationSnap;
_tempVector.z = Math.round( _tempVector.z / this.translationSnap ) * this.translationSnap;
}
if ( object.parent ) {
object.position.sub( _tempVector.setFromMatrixPosition( object.parent.matrixWorld ) );
}
object.position.copy( object.parent.worldToLocal( _tempVector ) );
}
}
object.position.x = Math.max( this.minX, Math.min( this.maxX, object.position.x ) );
object.position.y = Math.max( this.minY, Math.min( this.maxY, object.position.y ) );
object.position.z = Math.max( this.minZ, Math.min( this.maxZ, object.position.z ) );
} else if ( mode === 'scale' ) {
if ( axis.search( 'XYZ' ) !== - 1 ) {
@@ -539,7 +772,7 @@ class TransformControls extends Object3D {
pointerUp( pointer ) {
if ( pointer.button !== 0 ) return;
if ( pointer !== null && pointer.button !== 0 ) return;
if ( this.dragging && ( this.axis !== null ) ) {
@@ -555,41 +788,46 @@ class TransformControls extends Object3D {
dispose() {
this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
this.domElement.removeEventListener( 'pointermove', this._onPointerHover );
this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
this.disconnect();
this.traverse( function ( child ) {
if ( child.geometry ) child.geometry.dispose();
if ( child.material ) child.material.dispose();
} );
this._root.dispose();
}
// Set current object
/**
* Sets the 3D object that should be transformed and ensures the controls UI is visible.
*
* @param {Object3D} object - The 3D object that should be transformed.
* @return {TransformControls} A reference to this controls.
*/
attach( object ) {
this.object = object;
this.visible = true;
this._root.visible = true;
return this;
}
// Detach from object
/**
* Removes the current 3D object from the controls and makes the helper UI invisible.
*
* @return {TransformControls} A reference to this controls.
*/
detach() {
this.object = undefined;
this.visible = false;
this.axis = null;
this._root.visible = false;
return this;
}
/**
* Resets the object's position, rotation and scale to when the current transform began.
*/
reset() {
if ( ! this.enabled ) return;
@@ -609,56 +847,129 @@ class TransformControls extends Object3D {
}
/**
* Returns the raycaster that is used for user interaction. This object is shared between all
* instances of `TransformControls`.
*
* @returns {Raycaster} The internal raycaster.
*/
getRaycaster() {
return _raycaster;
}
// TODO: deprecate
/**
* Returns the transformation mode.
*
* @returns {'translate'|'rotate'|'scale'} The transformation mode.
*/
getMode() {
return this.mode;
}
/**
* Sets the given transformation mode.
*
* @param {'translate'|'rotate'|'scale'} mode - The transformation mode to set.
*/
setMode( mode ) {
this.mode = mode;
}
/**
* Sets the translation snap.
*
* @param {?number} translationSnap - The translation snap to set.
*/
setTranslationSnap( translationSnap ) {
this.translationSnap = translationSnap;
}
/**
* Sets the rotation snap.
*
* @param {?number} rotationSnap - The rotation snap to set.
*/
setRotationSnap( rotationSnap ) {
this.rotationSnap = rotationSnap;
}
/**
* Sets the scale snap.
*
* @param {?number} scaleSnap - The scale snap to set.
*/
setScaleSnap( scaleSnap ) {
this.scaleSnap = scaleSnap;
}
/**
* Sets the size of the helper UI.
*
* @param {number} size - The size to set.
*/
setSize( size ) {
this.size = size;
}
/**
* Sets the coordinate space in which transformations are applied.
*
* @param {'world'|'local'} space - The space to set.
*/
setSpace( space ) {
this.space = space;
}
/**
* Sets the colors of the control's gizmo.
*
* @param {number|Color|string} xAxis - The x-axis color.
* @param {number|Color|string} yAxis - The y-axis color.
* @param {number|Color|string} zAxis - The z-axis color.
* @param {number|Color|string} active - The color for active elements.
*/
setColors( xAxis, yAxis, zAxis, active ) {
const materialLib = this._gizmo.materialLib;
materialLib.xAxis.color.set( xAxis );
materialLib.yAxis.color.set( yAxis );
materialLib.zAxis.color.set( zAxis );
materialLib.active.color.set( active );
materialLib.xAxisTransparent.color.set( xAxis );
materialLib.yAxisTransparent.color.set( yAxis );
materialLib.zAxisTransparent.color.set( zAxis );
materialLib.activeTransparent.color.set( active );
// update color caches
if ( materialLib.xAxis._color ) materialLib.xAxis._color.set( xAxis );
if ( materialLib.yAxis._color ) materialLib.yAxis._color.set( yAxis );
if ( materialLib.zAxis._color ) materialLib.zAxis._color.set( zAxis );
if ( materialLib.active._color ) materialLib.active._color.set( active );
if ( materialLib.xAxisTransparent._color ) materialLib.xAxisTransparent._color.set( xAxis );
if ( materialLib.yAxisTransparent._color ) materialLib.yAxisTransparent._color.set( yAxis );
if ( materialLib.zAxisTransparent._color ) materialLib.zAxisTransparent._color.set( zAxis );
if ( materialLib.activeTransparent._color ) materialLib.activeTransparent._color.set( active );
}
}
// mouse / touch event handlers
@@ -676,10 +987,29 @@ function getPointer( event ) {
} else {
const rect = this.domElement.getBoundingClientRect();
const viewport = this.viewport;
let originX, originY, regionWidth, regionHeight;
if ( viewport !== null ) {
originX = viewport.x;
originY = rect.height - viewport.y - viewport.w;
regionWidth = viewport.z;
regionHeight = viewport.w;
} else {
originX = 0;
originY = 0;
regionWidth = rect.width;
regionHeight = rect.height;
}
return {
x: ( event.clientX - rect.left ) / rect.width * 2 - 1,
y: - ( event.clientY - rect.top ) / rect.height * 2 + 1,
x: ( event.clientX - rect.left - originX ) / regionWidth * 2 - 1,
y: - ( event.clientY - rect.top - originY ) / regionHeight * 2 + 1,
button: event.button
};
@@ -778,6 +1108,84 @@ const _v1 = new Vector3();
const _v2 = new Vector3();
const _v3 = new Vector3();
class TransformControlsRoot extends Object3D {
constructor( controls ) {
super();
this.isTransformControlsRoot = true;
this.controls = controls;
this.visible = false;
}
// updateMatrixWorld updates key transformation variables
updateMatrixWorld( force ) {
const controls = this.controls;
if ( controls.object !== undefined ) {
controls.object.updateMatrixWorld();
if ( controls.object.parent === null ) {
console.error( 'TransformControls: The attached 3D object must be a part of the scene graph.' );
} else {
controls.object.parent.matrixWorld.decompose( controls._parentPosition, controls._parentQuaternion, controls._parentScale );
}
controls.object.matrixWorld.decompose( controls.worldPosition, controls.worldQuaternion, controls._worldScale );
controls._parentQuaternionInv.copy( controls._parentQuaternion ).invert();
controls._worldQuaternionInv.copy( controls.worldQuaternion ).invert();
}
controls.camera.updateMatrixWorld();
controls.camera.matrixWorld.decompose( controls.cameraPosition, controls.cameraQuaternion, controls._cameraScale );
if ( controls.camera.isOrthographicCamera ) {
controls.camera.getWorldDirection( controls.eye ).negate();
} else {
controls.eye.copy( controls.cameraPosition ).sub( controls.worldPosition ).normalize();
}
// Cancel out the parent's transform so the gizmo stays world-aligned.
if ( this.parent ) {
_tempMatrix.copy( this.parent.matrixWorld ).invert();
_tempMatrix.decompose( this.position, this.quaternion, this.scale );
}
super.updateMatrixWorld( force );
}
dispose() {
this.traverse( function ( child ) {
if ( child.geometry ) child.geometry.dispose();
if ( child.material ) child.material.dispose();
} );
}
}
class TransformControlsGizmo extends Object3D {
constructor() {
@@ -848,6 +1256,19 @@ class TransformControlsGizmo extends Object3D {
const matGray = gizmoMaterial.clone();
matGray.color.setHex( 0x787878 );
// materials in the below property are configurable via setColors()
this.materialLib = {
xAxis: matRed,
yAxis: matGreen,
zAxis: matBlue,
active: matYellow,
xAxisTransparent: matRedTransparent,
yAxisTransparent: matGreenTransparent,
zAxisTransparent: matBlueTransparent,
activeTransparent: matYellowTransparent
};
// reusable geometry
const arrowGeometry = new CylinderGeometry( 0, 0.04, 0.1, 12 );
@@ -902,16 +1323,16 @@ class TransformControlsGizmo extends Object3D {
[ new Mesh( lineGeometry2, matBlue ), null, [ Math.PI / 2, 0, 0 ]]
],
XYZ: [
[ new Mesh( new OctahedronGeometry( 0.1, 0 ), matWhiteTransparent.clone() ), [ 0, 0, 0 ]]
[ new Mesh( new OctahedronGeometry( 0.1, 0 ), matWhiteTransparent ), [ 0, 0, 0 ]]
],
XY: [
[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matBlueTransparent.clone() ), [ 0.15, 0.15, 0 ]]
[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matBlueTransparent ), [ 0.15, 0.15, 0 ]]
],
YZ: [
[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matRedTransparent.clone() ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]]
[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matRedTransparent ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]]
],
XZ: [
[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matGreenTransparent.clone() ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]]
[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matGreenTransparent ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]]
]
};
@@ -953,13 +1374,13 @@ class TransformControlsGizmo extends Object3D {
[ new Line( TranslateHelperGeometry(), matHelper ), null, null, null, 'helper' ]
],
X: [
[ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ]
[ new Line( lineGeometry, matHelper ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ]
],
Y: [
[ new Line( lineGeometry, matHelper.clone() ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ]
[ new Line( lineGeometry, matHelper ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ]
],
Z: [
[ new Line( lineGeometry, matHelper.clone() ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ]
[ new Line( lineGeometry, matHelper ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ]
]
};
@@ -983,7 +1404,7 @@ class TransformControlsGizmo extends Object3D {
const helperRotate = {
AXIS: [
[ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ]
[ new Line( lineGeometry, matHelper ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ]
]
};
@@ -1031,7 +1452,7 @@ class TransformControlsGizmo extends Object3D {
[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matGreenTransparent ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]]
],
XYZ: [
[ new Mesh( new BoxGeometry( 0.1, 0.1, 0.1 ), matWhiteTransparent.clone() ) ],
[ new Mesh( new BoxGeometry( 0.1, 0.1, 0.1 ), matWhiteTransparent ) ],
]
};
@@ -1064,13 +1485,13 @@ class TransformControlsGizmo extends Object3D {
const helperScale = {
X: [
[ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ]
[ new Line( lineGeometry, matHelper ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ]
],
Y: [
[ new Line( lineGeometry, matHelper.clone() ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ]
[ new Line( lineGeometry, matHelper ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ]
],
Z: [
[ new Line( lineGeometry, matHelper.clone() ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ]
[ new Line( lineGeometry, matHelper ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ]
]
};
@@ -1439,6 +1860,15 @@ class TransformControlsGizmo extends Object3D {
handle.visible = handle.visible && ( handle.name.indexOf( 'Z' ) === - 1 || this.showZ );
handle.visible = handle.visible && ( handle.name.indexOf( 'E' ) === - 1 || ( this.showX && this.showY && this.showZ ) );
// Hide disabled plane helpers
handle.visible = handle.visible && ( handle.name.indexOf( 'XY' ) === - 1 || this.showXY );
handle.visible = handle.visible && ( handle.name.indexOf( 'YZ' ) === - 1 || this.showYZ );
handle.visible = handle.visible && ( handle.name.indexOf( 'XZ' ) === - 1 || this.showXZ );
// Hide disabled rotation helpers
handle.visible = handle.visible && ( handle.name !== 'E' || this.showE );
handle.visible = handle.visible && ( handle.name !== 'XYZE' || this.showXYZE );
// highlight selected axis
handle.material._color = handle.material._color || handle.material.color.clone();
@@ -1451,7 +1881,7 @@ class TransformControlsGizmo extends Object3D {
if ( handle.name === this.axis ) {
handle.material.color.setHex( 0xffff00 );
handle.material.color.copy( this.materialLib.active.color );
handle.material.opacity = 1.0;
} else if ( this.axis.split( '' ).some( function ( a ) {
@@ -1460,7 +1890,7 @@ class TransformControlsGizmo extends Object3D {
} ) ) {
handle.material.color.setHex( 0xffff00 );
handle.material.color.copy( this.materialLib.active.color );
handle.material.opacity = 1.0;
}
@@ -1570,4 +2000,4 @@ class TransformControlsPlane extends Mesh {
}
export { TransformControls, TransformControlsGizmo, TransformControlsPlane };
export { TransformControls, TransformControlsGizmo, TransformControlsPlane };
@@ -5,10 +5,6 @@ import {
Quaternion,
} from 'three';
/**
* This is a class to check whether objects are in a selection area in 3D space
*/
const _frustum = new Frustum();
const _center = new Vector3();
@@ -33,34 +29,111 @@ const _matrix = new Matrix4();
const _quaternion = new Quaternion();
const _scale = new Vector3();
/**
* This class can be used to select 3D objects in a scene with a selection box.
* It is recommended to visualize the selected area with the help of {@link SelectionHelper}.
*
* ```js
* const selectionBox = new SelectionBox( camera, scene );
* const selectedObjects = selectionBox.select( startPoint, endPoint );
* ```
*
* @three_import import { SelectionBox } from 'three/addons/interactive/SelectionBox.js';
*/
class SelectionBox {
/**
* Constructs a new selection box.
*
* @param {Camera} camera - The camera the scene is rendered with.
* @param {Scene} scene - The scene.
* @param {number} [deep=Number.MAX_VALUE] - How deep the selection frustum of perspective cameras should extend.
*/
constructor( camera, scene, deep = Number.MAX_VALUE ) {
/**
* The camera the scene is rendered with.
*
* @type {Camera}
*/
this.camera = camera;
/**
* The camera the scene is rendered with.
*
* @type {Scene}
*/
this.scene = scene;
/**
* The start point of the selection.
*
* @type {Vector3}
*/
this.startPoint = new Vector3();
/**
* The end point of the selection.
*
* @type {Vector3}
*/
this.endPoint = new Vector3();
/**
* The selected 3D objects.
*
* @type {Array<Object3D>}
*/
this.collection = [];
/**
* The selected instance IDs of instanced meshes.
*
* @type {Object}
*/
this.instances = {};
/**
* The selected batches of batched meshes.
*
* @type {Object}
*/
this.batches = {};
/**
* How deep the selection frustum of perspective cameras should extend.
*
* @type {number}
* @default Number.MAX_VALUE
*/
this.deep = deep;
}
/**
* This method selects 3D objects in the scene based on the given start
* and end point. If no parameters are provided, the method uses the start
* and end values of the respective members.
*
* @param {Vector3} [startPoint] - The start point.
* @param {Vector3} [endPoint] - The end point.
* @return {Array<Object3D>} The selected 3D objects.
*/
select( startPoint, endPoint ) {
this.startPoint = startPoint || this.startPoint;
this.endPoint = endPoint || this.endPoint;
this.collection = [];
this.updateFrustum( this.startPoint, this.endPoint );
this.searchChildInFrustum( _frustum, this.scene );
this._updateFrustum( this.startPoint, this.endPoint );
this._searchChildInFrustum( _frustum, this.scene );
return this.collection;
}
updateFrustum( startPoint, endPoint ) {
// private
_updateFrustum( startPoint, endPoint ) {
startPoint = startPoint || this.startPoint;
endPoint = endPoint || this.endPoint;
@@ -170,7 +243,7 @@ class SelectionBox {
}
searchChildInFrustum( frustum, object ) {
_searchChildInFrustum( frustum, object ) {
if ( object.isMesh || object.isLine || object.isPoints ) {
@@ -192,6 +265,30 @@ class SelectionBox {
}
} else if ( object.isBatchedMesh ) {
this.batches[ object.uuid ] = [];
for ( let instanceId = 0, count = 0; count < object.instanceCount; instanceId ++ ) {
// skip invalid instances in the batchedMesh
if ( object.validateInstanceId( instanceId ) === false ) continue;
count ++;
object.getMatrixAt( instanceId, _matrix );
_matrix.decompose( _center, _quaternion, _scale );
_center.applyMatrix4( object.matrixWorld );
if ( frustum.containsPoint( _center ) ) {
this.batches[ object.uuid ].push( instanceId );
}
}
} else {
if ( object.geometry.boundingSphere === null ) object.geometry.computeBoundingSphere();
@@ -214,7 +311,7 @@ class SelectionBox {
for ( let x = 0; x < object.children.length; x ++ ) {
this.searchChildInFrustum( frustum, object.children[ x ] );
this._searchChildInFrustum( frustum, object.children[ x ] );
}
@@ -2,21 +2,73 @@ import {
BufferAttribute,
BufferGeometry,
Color,
ColorManagement,
FileLoader,
Loader,
LinearSRGBColorSpace,
SRGBColorSpace
SRGBColorSpace,
InterleavedBuffer,
InterleavedBufferAttribute,
LoaderUtils
} from 'three';
const _taskCache = new WeakMap();
const WASM_BIN_URL = new URL( '../libs/draco/draco_decoder.wasm', import.meta.url ).toString();
const WASM_JS_URL = new URL( '../libs/draco/draco_wasm_wrapper.js', import.meta.url ).toString();
const JS_URL = new URL( '../libs/draco/draco_decoder.js', import.meta.url ).toString();
const DRACO_GLTF_CONFIG = {
js: new URL( '../libs/draco/gltf/draco_wasm_wrapper.js', import.meta.url ).toString(),
wasm: new URL( '../libs/draco/gltf/draco_decoder.wasm', import.meta.url ).toString(),
};
/**
* A loader for the Draco format.
*
* [Draco](https://google.github.io/draco/) is an open source library for compressing
* and decompressing 3D meshes and point clouds. Compressed geometry can be significantly smaller,
* at the cost of additional decoding time on the client device.
*
* Standalone Draco files have a `.drc` extension, and contain vertex positions, normals, colors,
* and other attributes. Draco files do not contain materials, textures, animation, or node hierarchies –
* to use these features, embed Draco geometry inside of a glTF file. A normal glTF file can be converted
* to a Draco-compressed glTF file using [glTF-Pipeline](https://github.com/CesiumGS/gltf-pipeline).
* When using Draco with glTF, an instance of `DRACOLoader` will be used internally by {@link GLTFLoader}.
*
* It is recommended to create one DRACOLoader instance and reuse it to avoid loading and creating
* multiple decoder instances.
*
* `DRACOLoader` will automatically use either the JS or the WASM decoding library, based on
* browser capabilities.
*
* ```js
* const loader = new DRACOLoader();
* const geometry = await loader.loadAsync( 'models/draco/bunny.drc' );
* geometry.computeVertexNormals(); // optional
*
* loader.dispose();
* ```
*
* @augments Loader
* @three_import import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
*/
class DRACOLoader extends Loader {
/**
* Constructs a new Draco loader.
*
* @param {LoadingManager} [manager] - The loading manager.
*/
constructor( manager ) {
super( manager );
this.decoderPath = '';
this.decoderPaths = {
js: WASM_JS_URL,
wasm: WASM_BIN_URL,
dep_js: JS_URL,
};
this.decoderConfig = {};
this.decoderBinary = null;
this.decoderPending = null;
@@ -41,22 +93,56 @@ class DRACOLoader extends Loader {
}
/**
* Provides configuration for the decoder libraries. Configuration cannot be changed after decoding begins.
*
* @param {string|{js:string, wasm:string}} path - The decoder path, or a config object with explicit URLs for each decoder file.
* @return {DRACOLoader} A reference to this loader.
*/
setDecoderPath( path ) {
this.decoderPath = path;
const { decoderPaths } = this;
if ( typeof path === 'object' ) {
decoderPaths.js = path.js;
decoderPaths.wasm = path.wasm;
decoderPaths.dep_js = null;
} else {
decoderPaths.js = LoaderUtils.resolveURL( 'draco_wasm_wrapper.js', path );
decoderPaths.wasm = LoaderUtils.resolveURL( 'draco_decoder.wasm', path );
decoderPaths.dep_js = LoaderUtils.resolveURL( 'draco_decoder.js', path );
}
return this;
}
/**
* Provides configuration for the decoder libraries. Configuration cannot be changed after decoding begins.
*
* @deprecated
* @param {{type:('js'|'wasm')}} config - The decoder config.
* @return {DRACOLoader} A reference to this loader.
*/
setDecoderConfig( config ) {
console.warn( 'THREE.DRACOLoader: setDecoderConfig to has been deprecated and will be removed in r194.' );
this.decoderConfig = config;
return this;
}
/**
* Sets the maximum number of Web Workers to be used during decoding.
* A lower limit may be preferable if workers are also for other tasks in the application.
*
* @param {number} workerLimit - The worker limit.
* @return {DRACOLoader} A reference to this loader.
*/
setWorkerLimit( workerLimit ) {
this.workerLimit = workerLimit;
@@ -65,6 +151,15 @@ class DRACOLoader extends Loader {
}
/**
* Starts loading from the given URL and passes the loaded Draco asset
* to the `onLoad()` callback.
*
* @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
* @param {function(BufferGeometry)} onLoad - Executed when the loading process has been finished.
* @param {onProgressCallback} onProgress - Executed while the loading is in progress.
* @param {onErrorCallback} onError - Executed when errors occur.
*/
load( url, onLoad, onProgress, onError ) {
const loader = new FileLoader( this.manager );
@@ -82,13 +177,21 @@ class DRACOLoader extends Loader {
}
/**
* Parses the given Draco data.
*
* @param {ArrayBuffer} buffer - The raw Draco data as an array buffer.
* @param {function(BufferGeometry)} onLoad - Executed when the loading/parsing process has been finished.
* @param {onErrorCallback} onError - Executed when errors occur.
*/
parse( buffer, onLoad, onError = ()=>{} ) {
this.decodeDracoFile( buffer, onLoad, null, null, SRGBColorSpace ).catch( onError );
this.decodeDracoFile( buffer, onLoad, null, null, SRGBColorSpace, onError ).catch( onError );
}
//
decodeDracoFile( buffer, callback, attributeIDs, attributeTypes, vertexColorSpace = LinearSRGBColorSpace, onError = () => {} ) {
const taskConfig = {
@@ -199,16 +302,25 @@ class DRACOLoader extends Loader {
for ( let i = 0; i < geometryData.attributes.length; i ++ ) {
const result = geometryData.attributes[ i ];
const name = result.name;
const array = result.array;
const itemSize = result.itemSize;
const { name, array, itemSize, stride, vertexColorSpace } = geometryData.attributes[ i ];
const attribute = new BufferAttribute( array, itemSize );
let attribute;
if ( itemSize === stride ) {
attribute = new BufferAttribute( array, itemSize );
} else {
const buffer = new InterleavedBuffer( array, stride );
attribute = new InterleavedBufferAttribute( buffer, itemSize, 0 );
}
if ( name === 'color' ) {
this._assignVertexColorSpace( attribute, result.vertexColorSpace );
this._assignVertexColorSpace( attribute, vertexColorSpace );
attribute.normalized = ( array instanceof Float32Array ) === false;
@@ -235,7 +347,8 @@ class DRACOLoader extends Loader {
for ( let i = 0, il = attribute.count; i < il; i ++ ) {
_color.fromBufferAttribute( attribute, i ).convertSRGBToLinear();
_color.fromBufferAttribute( attribute, i );
ColorManagement.colorSpaceToWorking( _color, SRGBColorSpace );
attribute.setXYZ( i, _color.r, _color.g, _color.b );
}
@@ -245,7 +358,6 @@ class DRACOLoader extends Loader {
_loadLibrary( url, responseType ) {
const loader = new FileLoader( this.manager );
loader.setPath( this.decoderPath );
loader.setResponseType( responseType );
loader.setWithCredentials( this.withCredentials );
@@ -272,14 +384,21 @@ class DRACOLoader extends Loader {
const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js';
const librariesPending = [];
const { decoderPaths } = this;
if ( useJS ) {
librariesPending.push( this._loadLibrary( 'draco_decoder.js', 'text' ) );
if ( decoderPaths.dep_js === null ) {
throw new Error( 'THREE.DRACOLoader: WebAssembly is required when using a custom decoder paths.' );
}
librariesPending.push( this._loadLibrary( decoderPaths.dep_js, 'text' ) );
} else {
librariesPending.push( this._loadLibrary( 'draco_wasm_wrapper.js', 'text' ) );
librariesPending.push( this._loadLibrary( 'draco_decoder.wasm', 'arraybuffer' ) );
librariesPending.push( this._loadLibrary( decoderPaths.js, 'text' ) );
librariesPending.push( this._loadLibrary( decoderPaths.wasm, 'arraybuffer' ) );
}
@@ -571,30 +690,70 @@ function DRACOWorker() {
}
function decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) {
function decodeAttribute( draco, decoder, dracoGeometry, attributeName, TypedArray, attribute ) {
const numComponents = attribute.num_components();
const numPoints = dracoGeometry.num_points();
const numValues = numPoints * numComponents;
const byteLength = numValues * attributeType.BYTES_PER_ELEMENT;
const dataType = getDracoDataType( draco, attributeType );
const count = dracoGeometry.num_points();
const itemSize = attribute.num_components();
const dracoDataType = getDracoDataType( draco, TypedArray );
// Reference: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#data-alignment
const srcByteStride = itemSize * TypedArray.BYTES_PER_ELEMENT;
const dstByteStride = Math.ceil( srcByteStride / 4 ) * 4;
const dstStride = dstByteStride / TypedArray.BYTES_PER_ELEMENT;
const srcByteLength = count * srcByteStride;
const dstByteLength = count * dstByteStride;
const ptr = draco._malloc( srcByteLength );
decoder.GetAttributeDataArrayForAllPoints( dracoGeometry, attribute, dracoDataType, srcByteLength, ptr );
const srcArray = new TypedArray( draco.HEAPF32.buffer, ptr, srcByteLength / TypedArray.BYTES_PER_ELEMENT );
let dstArray;
if ( srcByteStride === dstByteStride ) {
// THREE.BufferAttribute
dstArray = srcArray.slice();
} else {
// THREE.InterleavedBufferAttribute
dstArray = new TypedArray( dstByteLength / TypedArray.BYTES_PER_ELEMENT );
let dstOffset = 0;
for ( let i = 0, il = srcArray.length; i < il; i ++ ) {
for ( let j = 0; j < itemSize; j ++ ) {
dstArray[ dstOffset + j ] = srcArray[ i * itemSize + j ];
}
dstOffset += dstStride;
}
}
const ptr = draco._malloc( byteLength );
decoder.GetAttributeDataArrayForAllPoints( dracoGeometry, attribute, dataType, byteLength, ptr );
const array = new attributeType( draco.HEAPF32.buffer, ptr, numValues ).slice();
draco._free( ptr );
return {
name: attributeName,
array: array,
itemSize: numComponents
count: count,
itemSize: itemSize,
array: dstArray,
stride: dstStride
};
}
function getDracoDataType( draco, attributeType ) {
function getDracoDataType( draco, TypedArray ) {
switch ( attributeType ) {
switch ( TypedArray ) {
case Float32Array: return draco.DT_FLOAT32;
case Int8Array: return draco.DT_INT8;
@@ -610,4 +769,4 @@ function DRACOWorker() {
}
export { DRACOLoader };
export { DRACOLoader, DRACO_GLTF_CONFIG };
File diff suppressed because it is too large Load Diff
@@ -1,19 +1,61 @@
import {
Clock,
HalfFloatType,
NoBlending,
Timer,
Vector2,
WebGLRenderTarget
} from 'three';
import { CopyShader } from '../shaders/CopyShader.js';
import { ShaderPass } from './ShaderPass.js';
import { MaskPass } from './MaskPass.js';
import { ClearMaskPass } from './MaskPass.js';
import { ClearMaskPass, MaskPass } from './MaskPass.js';
/**
* Used to implement post-processing effects in three.js.
* The class manages a chain of post-processing passes to produce the final visual result.
* Post-processing passes are executed in order of their addition/insertion.
* The last pass is automatically rendered to screen.
*
* This module can only be used with {@link WebGLRenderer}.
*
* ```js
* const composer = new EffectComposer( renderer );
*
* // adding some passes
* const renderPass = new RenderPass( scene, camera );
* composer.addPass( renderPass );
*
* const glitchPass = new GlitchPass();
* composer.addPass( glitchPass );
*
* const outputPass = new OutputPass()
* composer.addPass( outputPass );
*
* function animate() {
*
* composer.render(); // instead of renderer.render()
*
* }
* ```
*
* @three_import import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
*/
class EffectComposer {
/**
* Constructs a new effect composer.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} [renderTarget] - This render target and a clone will
* be used as the internal read and write buffers. If not given, the composer creates
* the buffers automatically.
*/
constructor( renderer, renderTarget ) {
/**
* The renderer.
*
* @type {WebGLRenderer}
*/
this.renderer = renderer;
this._pixelRatio = renderer.getPixelRatio();
@@ -38,20 +80,59 @@ class EffectComposer {
this.renderTarget2 = renderTarget.clone();
this.renderTarget2.texture.name = 'EffectComposer.rt2';
/**
* A reference to the internal write buffer. Passes usually write
* their result into this buffer.
*
* @type {WebGLRenderTarget}
*/
this.writeBuffer = this.renderTarget1;
/**
* A reference to the internal read buffer. Passes usually read
* the previous render result from this buffer.
*
* @type {WebGLRenderTarget}
*/
this.readBuffer = this.renderTarget2;
/**
* Whether the final pass is rendered to the screen (default framebuffer) or not.
*
* @type {boolean}
* @default true
*/
this.renderToScreen = true;
/**
* An array representing the (ordered) chain of post-processing passes.
*
* @type {Array<Pass>}
*/
this.passes = [];
/**
* A copy pass used for internal swap operations.
*
* @private
* @type {ShaderPass}
*/
this.copyPass = new ShaderPass( CopyShader );
this.copyPass.material.blending = NoBlending;
this.clock = new Clock();
/**
* The internal timer for managing time data.
*
* @private
* @type {Timer}
*/
this.timer = new Timer();
}
/**
* Swaps the internal read/write buffers.
*/
swapBuffers() {
const tmp = this.readBuffer;
@@ -60,6 +141,11 @@ class EffectComposer {
}
/**
* Adds the given pass to the pass chain.
*
* @param {Pass} pass - The pass to add.
*/
addPass( pass ) {
this.passes.push( pass );
@@ -67,6 +153,12 @@ class EffectComposer {
}
/**
* Inserts the given pass at a given index.
*
* @param {Pass} pass - The pass to insert.
* @param {number} index - The index into the pass chain.
*/
insertPass( pass, index ) {
this.passes.splice( index, 0, pass );
@@ -74,6 +166,11 @@ class EffectComposer {
}
/**
* Removes the given pass from the pass chain.
*
* @param {Pass} pass - The pass to remove.
*/
removePass( pass ) {
const index = this.passes.indexOf( pass );
@@ -86,6 +183,12 @@ class EffectComposer {
}
/**
* Returns `true` if the pass for the given index is the last enabled pass in the pass chain.
*
* @param {number} passIndex - The pass index.
* @return {boolean} Whether the pass for the given index is the last pass in the pass chain.
*/
isLastEnabledPass( passIndex ) {
for ( let i = passIndex + 1; i < this.passes.length; i ++ ) {
@@ -102,13 +205,21 @@ class EffectComposer {
}
/**
* Executes all enabled post-processing passes in order to produce the final frame.
*
* @param {number} deltaTime - The delta time in seconds. If not given, the composer computes
* its own time delta value.
*/
render( deltaTime ) {
// deltaTime value is in seconds
this.timer.update();
if ( deltaTime === undefined ) {
deltaTime = this.clock.getDelta();
deltaTime = this.timer.getDelta();
}
@@ -166,6 +277,12 @@ class EffectComposer {
}
/**
* Resets the internal state of the EffectComposer.
*
* @param {WebGLRenderTarget} [renderTarget] - This render target has the same purpose like
* the one from the constructor. If set, it is used to setup the read and write buffers.
*/
reset( renderTarget ) {
if ( renderTarget === undefined ) {
@@ -190,6 +307,13 @@ class EffectComposer {
}
/**
* Resizes the internal read and write buffers as well as all passes. Similar to {@link WebGLRenderer#setSize},
* this method honors the current pixel ration.
*
* @param {number} width - The width in logical pixels.
* @param {number} height - The height in logical pixels.
*/
setSize( width, height ) {
this._width = width;
@@ -209,6 +333,12 @@ class EffectComposer {
}
/**
* Sets device pixel ratio. This is usually used for HiDPI device to prevent blurring output.
* Setting the pixel ratio will automatically resize the composer.
*
* @param {number} pixelRatio - The pixel ratio to set.
*/
setPixelRatio( pixelRatio ) {
this._pixelRatio = pixelRatio;
@@ -217,6 +347,10 @@ class EffectComposer {
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the composer is no longer used in your app.
*/
dispose() {
this.renderTarget1.dispose();
@@ -1,21 +1,82 @@
import { Pass } from './Pass.js';
/**
* This pass can be used to define a mask during post processing.
* Meaning only areas of subsequent post processing are affected
* which lie in the masking area of this pass. Internally, the masking
* is implemented with the stencil buffer.
*
* ```js
* const maskPass = new MaskPass( scene, camera );
* composer.addPass( maskPass );
* ```
*
* @augments Pass
* @three_import import { MaskPass } from 'three/addons/postprocessing/MaskPass.js';
*/
class MaskPass extends Pass {
/**
* Constructs a new mask pass.
*
* @param {Scene} scene - The 3D objects in this scene will define the mask.
* @param {Camera} camera - The camera.
*/
constructor( scene, camera ) {
super();
/**
* The scene that defines the mask.
*
* @type {Scene}
*/
this.scene = scene;
/**
* The camera.
*
* @type {Camera}
*/
this.camera = camera;
/**
* Overwritten to perform a clear operation by default.
*
* @type {boolean}
* @default true
*/
this.clear = true;
/**
* Overwritten to disable the swap.
*
* @type {boolean}
* @default false
*/
this.needsSwap = false;
/**
* Whether to inverse the mask or not.
*
* @type {boolean}
* @default false
*/
this.inverse = false;
}
/**
* Performs a mask pass with the configured scene and camera.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
const context = renderer.getContext();
@@ -82,16 +143,46 @@ class MaskPass extends Pass {
}
/**
* This pass can be used to clear a mask previously defined with {@link MaskPass}.
*
* ```js
* const clearPass = new ClearMaskPass();
* composer.addPass( clearPass );
* ```
*
* @augments Pass
*/
class ClearMaskPass extends Pass {
/**
* Constructs a new clear mask pass.
*/
constructor() {
super();
/**
* Overwritten to disable the swap.
*
* @type {boolean}
* @default false
*/
this.needsSwap = false;
}
/**
* Performs the clear of the currently defined mask.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
renderer.state.buffers.stencil.setLocked( false );
@@ -16,27 +16,138 @@ import {
import { Pass, FullScreenQuad } from './Pass.js';
import { CopyShader } from '../shaders/CopyShader.js';
/**
* A pass for rendering outlines around selected objects.
*
* ```js
* const resolution = new THREE.Vector2( window.innerWidth, window.innerHeight );
* const outlinePass = new OutlinePass( resolution, scene, camera );
* composer.addPass( outlinePass );
* ```
*
* @augments Pass
* @three_import import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js';
*/
class OutlinePass extends Pass {
/**
* Constructs a new outline pass.
*
* @param {Vector2} [resolution] - The effect's resolution.
* @param {Scene} scene - The scene to render.
* @param {Camera} camera - The camera.
* @param {Array<Object3D>} [selectedObjects] - The selected 3D objects that should receive an outline.
*
*/
constructor( resolution, scene, camera, selectedObjects ) {
super();
/**
* The scene to render.
*
* @type {Object}
*/
this.renderScene = scene;
/**
* The camera.
*
* @type {Object}
*/
this.renderCamera = camera;
/**
* The selected 3D objects that should receive an outline.
*
* @type {Array<Object3D>}
*/
this.selectedObjects = selectedObjects !== undefined ? selectedObjects : [];
/**
* The visible edge color.
*
* @type {Color}
* @default (1,1,1)
*/
this.visibleEdgeColor = new Color( 1, 1, 1 );
/**
* The hidden edge color.
*
* @type {Color}
* @default (0.1,0.04,0.02)
*/
this.hiddenEdgeColor = new Color( 0.1, 0.04, 0.02 );
/**
* Can be used for an animated glow/pulse effect.
*
* @type {number}
* @default 0
*/
this.edgeGlow = 0.0;
/**
* Whether to use a pattern texture for to highlight selected
* 3D objects or not.
*
* @type {boolean}
* @default false
*/
this.usePatternTexture = false;
/**
* Can be used to highlight selected 3D objects. Requires to set
* {@link OutlinePass#usePatternTexture} to `true`.
*
* @type {?Texture}
* @default null
*/
this.patternTexture = null;
/**
* The edge thickness.
*
* @type {number}
* @default 1
*/
this.edgeThickness = 1.0;
/**
* The edge strength.
*
* @type {number}
* @default 3
*/
this.edgeStrength = 3.0;
/**
* The downsample ratio. The effect can be rendered in a much
* lower resolution than the beauty pass.
*
* @type {number}
* @default 2
*/
this.downSampleRatio = 2;
/**
* The pulse period.
*
* @type {number}
* @default 0
*/
this.pulsePeriod = 0;
this._visibilityCache = new Map();
this._selectionCache = new Set();
/**
* The effect's resolution.
*
* @type {Vector2}
* @default (256,256)
*/
this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 );
const resx = Math.round( this.resolution.x / this.downSampleRatio );
@@ -51,7 +162,7 @@ class OutlinePass extends Pass {
this.depthMaterial.depthPacking = RGBADepthPacking;
this.depthMaterial.blending = NoBlending;
this.prepareMaskMaterial = this.getPrepareMaskMaterial();
this.prepareMaskMaterial = this._getPrepareMaskMaterial();
this.prepareMaskMaterial.side = DoubleSide;
this.prepareMaskMaterial.fragmentShader = replaceDepthToViewZ( this.prepareMaskMaterial.fragmentShader, this.renderCamera );
@@ -70,7 +181,7 @@ class OutlinePass extends Pass {
this.renderTargetBlurBuffer2.texture.name = 'OutlinePass.blur2';
this.renderTargetBlurBuffer2.texture.generateMipmaps = false;
this.edgeDetectionMaterial = this.getEdgeDetectionMaterial();
this.edgeDetectionMaterial = this._getEdgeDetectionMaterial();
this.renderTargetEdgeBuffer1 = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
this.renderTargetEdgeBuffer1.texture.name = 'OutlinePass.edge1';
this.renderTargetEdgeBuffer1.texture.generateMipmaps = false;
@@ -81,15 +192,15 @@ class OutlinePass extends Pass {
const MAX_EDGE_THICKNESS = 4;
const MAX_EDGE_GLOW = 4;
this.separableBlurMaterial1 = this.getSeperableBlurMaterial( MAX_EDGE_THICKNESS );
this.separableBlurMaterial1 = this._getSeparableBlurMaterial( MAX_EDGE_THICKNESS );
this.separableBlurMaterial1.uniforms[ 'texSize' ].value.set( resx, resy );
this.separableBlurMaterial1.uniforms[ 'kernelRadius' ].value = 1;
this.separableBlurMaterial2 = this.getSeperableBlurMaterial( MAX_EDGE_GLOW );
this.separableBlurMaterial2 = this._getSeparableBlurMaterial( MAX_EDGE_GLOW );
this.separableBlurMaterial2.uniforms[ 'texSize' ].value.set( Math.round( resx / 2 ), Math.round( resy / 2 ) );
this.separableBlurMaterial2.uniforms[ 'kernelRadius' ].value = MAX_EDGE_GLOW;
// Overlay material
this.overlayMaterial = this.getOverlayMaterial();
this.overlayMaterial = this._getOverlayMaterial();
// copy material
@@ -112,7 +223,7 @@ class OutlinePass extends Pass {
this._oldClearColor = new Color();
this.oldClearAlpha = 1;
this.fsQuad = new FullScreenQuad( null );
this._fsQuad = new FullScreenQuad( null );
this.tempPulseColor1 = new Color();
this.tempPulseColor2 = new Color();
@@ -128,6 +239,10 @@ class OutlinePass extends Pass {
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*/
dispose() {
this.renderTargetMaskBuffer.dispose();
@@ -146,10 +261,16 @@ class OutlinePass extends Pass {
this.overlayMaterial.dispose();
this.materialCopy.dispose();
this.fsQuad.dispose();
this._fsQuad.dispose();
}
/**
* Sets the size of the pass.
*
* @param {number} width - The width to set.
* @param {number} height - The height to set.
*/
setSize( width, height ) {
this.renderTargetMaskBuffer.setSize( width, height );
@@ -172,126 +293,17 @@ class OutlinePass extends Pass {
}
changeVisibilityOfSelectedObjects( bVisible ) {
const cache = this._visibilityCache;
function gatherSelectedMeshesCallBack( object ) {
if ( object.isMesh ) {
if ( bVisible === true ) {
object.visible = cache.get( object );
} else {
cache.set( object, object.visible );
object.visible = bVisible;
}
}
}
for ( let i = 0; i < this.selectedObjects.length; i ++ ) {
const selectedObject = this.selectedObjects[ i ];
selectedObject.traverse( gatherSelectedMeshesCallBack );
}
}
changeVisibilityOfNonSelectedObjects( bVisible ) {
const cache = this._visibilityCache;
const selectedMeshes = [];
function gatherSelectedMeshesCallBack( object ) {
if ( object.isMesh ) selectedMeshes.push( object );
}
for ( let i = 0; i < this.selectedObjects.length; i ++ ) {
const selectedObject = this.selectedObjects[ i ];
selectedObject.traverse( gatherSelectedMeshesCallBack );
}
function VisibilityChangeCallBack( object ) {
if ( object.isMesh || object.isSprite ) {
// only meshes and sprites are supported by OutlinePass
let bFound = false;
for ( let i = 0; i < selectedMeshes.length; i ++ ) {
const selectedObjectId = selectedMeshes[ i ].id;
if ( selectedObjectId === object.id ) {
bFound = true;
break;
}
}
if ( bFound === false ) {
const visibility = object.visible;
if ( bVisible === false || cache.get( object ) === true ) {
object.visible = bVisible;
}
cache.set( object, visibility );
}
} else if ( object.isPoints || object.isLine || object.isTransformControls ) {
// the visibilty of points and lines is always set to false in order to
// not affect the outline computation
if ( bVisible === true ) {
object.visible = cache.get( object ); // restore
} else {
cache.set( object, object.visible );
object.visible = bVisible;
}
}
}
this.renderScene.traverse( VisibilityChangeCallBack );
}
updateTextureMatrix() {
this.textureMatrix.set( 0.5, 0.0, 0.0, 0.5,
0.0, 0.5, 0.0, 0.5,
0.0, 0.0, 0.5, 0.5,
0.0, 0.0, 0.0, 1.0 );
this.textureMatrix.multiply( this.renderCamera.projectionMatrix );
this.textureMatrix.multiply( this.renderCamera.matrixWorldInverse );
}
/**
* Performs the Outline pass.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {
if ( this.selectedObjects.length > 0 ) {
@@ -306,10 +318,13 @@ class OutlinePass extends Pass {
renderer.setClearColor( 0xffffff, 1 );
this._updateSelectionCache();
// Make selected objects invisible
this.changeVisibilityOfSelectedObjects( false );
this._changeVisibilityOfSelectedObjects( false );
const currentBackground = this.renderScene.background;
const currentOverrideMaterial = this.renderScene.overrideMaterial;
this.renderScene.background = null;
// 1. Draw Non Selected objects in the depth buffer
@@ -319,14 +334,14 @@ class OutlinePass extends Pass {
renderer.render( this.renderScene, this.renderCamera );
// Make selected objects visible
this.changeVisibilityOfSelectedObjects( true );
this._changeVisibilityOfSelectedObjects( true );
this._visibilityCache.clear();
// Update Texture Matrix for Depth compare
this.updateTextureMatrix();
this._updateTextureMatrix();
// Make non selected objects invisible, and draw only the selected objects, by comparing the depth buffer of non selected objects
this.changeVisibilityOfNonSelectedObjects( false );
this._changeVisibilityOfNonSelectedObjects( false );
this.renderScene.overrideMaterial = this.prepareMaskMaterial;
this.prepareMaskMaterial.uniforms[ 'cameraNearFar' ].value.set( this.renderCamera.near, this.renderCamera.far );
this.prepareMaskMaterial.uniforms[ 'depthTexture' ].value = this.renderTargetDepthBuffer.texture;
@@ -334,18 +349,19 @@ class OutlinePass extends Pass {
renderer.setRenderTarget( this.renderTargetMaskBuffer );
renderer.clear();
renderer.render( this.renderScene, this.renderCamera );
this.renderScene.overrideMaterial = null;
this.changeVisibilityOfNonSelectedObjects( true );
this._changeVisibilityOfNonSelectedObjects( true );
this._visibilityCache.clear();
this._selectionCache.clear();
this.renderScene.background = currentBackground;
this.renderScene.overrideMaterial = currentOverrideMaterial;
// 2. Downsample to Half resolution
this.fsQuad.material = this.materialCopy;
this._fsQuad.material = this.materialCopy;
this.copyUniforms[ 'tDiffuse' ].value = this.renderTargetMaskBuffer.texture;
renderer.setRenderTarget( this.renderTargetMaskDownSampleBuffer );
renderer.clear();
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
this.tempPulseColor1.copy( this.visibleEdgeColor );
this.tempPulseColor2.copy( this.hiddenEdgeColor );
@@ -359,44 +375,44 @@ class OutlinePass extends Pass {
}
// 3. Apply Edge Detection Pass
this.fsQuad.material = this.edgeDetectionMaterial;
this._fsQuad.material = this.edgeDetectionMaterial;
this.edgeDetectionMaterial.uniforms[ 'maskTexture' ].value = this.renderTargetMaskDownSampleBuffer.texture;
this.edgeDetectionMaterial.uniforms[ 'texSize' ].value.set( this.renderTargetMaskDownSampleBuffer.width, this.renderTargetMaskDownSampleBuffer.height );
this.edgeDetectionMaterial.uniforms[ 'visibleEdgeColor' ].value = this.tempPulseColor1;
this.edgeDetectionMaterial.uniforms[ 'hiddenEdgeColor' ].value = this.tempPulseColor2;
renderer.setRenderTarget( this.renderTargetEdgeBuffer1 );
renderer.clear();
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
// 4. Apply Blur on Half res
this.fsQuad.material = this.separableBlurMaterial1;
this._fsQuad.material = this.separableBlurMaterial1;
this.separableBlurMaterial1.uniforms[ 'colorTexture' ].value = this.renderTargetEdgeBuffer1.texture;
this.separableBlurMaterial1.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionX;
this.separableBlurMaterial1.uniforms[ 'kernelRadius' ].value = this.edgeThickness;
renderer.setRenderTarget( this.renderTargetBlurBuffer1 );
renderer.clear();
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
this.separableBlurMaterial1.uniforms[ 'colorTexture' ].value = this.renderTargetBlurBuffer1.texture;
this.separableBlurMaterial1.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionY;
renderer.setRenderTarget( this.renderTargetEdgeBuffer1 );
renderer.clear();
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
// Apply Blur on quarter res
this.fsQuad.material = this.separableBlurMaterial2;
this._fsQuad.material = this.separableBlurMaterial2;
this.separableBlurMaterial2.uniforms[ 'colorTexture' ].value = this.renderTargetEdgeBuffer1.texture;
this.separableBlurMaterial2.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionX;
renderer.setRenderTarget( this.renderTargetBlurBuffer2 );
renderer.clear();
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
this.separableBlurMaterial2.uniforms[ 'colorTexture' ].value = this.renderTargetBlurBuffer2.texture;
this.separableBlurMaterial2.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionY;
renderer.setRenderTarget( this.renderTargetEdgeBuffer2 );
renderer.clear();
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
// Blend it additively over the input texture
this.fsQuad.material = this.overlayMaterial;
this._fsQuad.material = this.overlayMaterial;
this.overlayMaterial.uniforms[ 'maskTexture' ].value = this.renderTargetMaskBuffer.texture;
this.overlayMaterial.uniforms[ 'edgeTexture1' ].value = this.renderTargetEdgeBuffer1.texture;
this.overlayMaterial.uniforms[ 'edgeTexture2' ].value = this.renderTargetEdgeBuffer2.texture;
@@ -409,7 +425,7 @@ class OutlinePass extends Pass {
if ( maskActive ) renderer.state.buffers.stencil.setTest( true );
renderer.setRenderTarget( readBuffer );
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
renderer.setClearColor( this._oldClearColor, this.oldClearAlpha );
renderer.autoClear = oldAutoClear;
@@ -418,16 +434,120 @@ class OutlinePass extends Pass {
if ( this.renderToScreen ) {
this.fsQuad.material = this.materialCopy;
this._fsQuad.material = this.materialCopy;
this.copyUniforms[ 'tDiffuse' ].value = readBuffer.texture;
renderer.setRenderTarget( null );
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
}
}
getPrepareMaskMaterial() {
// internals
_updateSelectionCache() {
const cache = this._selectionCache;
function gatherSelectedMeshesCallBack( object ) {
if ( object.isMesh ) cache.add( object );
}
cache.clear();
for ( let i = 0; i < this.selectedObjects.length; i ++ ) {
const selectedObject = this.selectedObjects[ i ];
selectedObject.traverse( gatherSelectedMeshesCallBack );
}
}
_changeVisibilityOfSelectedObjects( bVisible ) {
const cache = this._visibilityCache;
for ( const mesh of this._selectionCache ) {
if ( bVisible === true ) {
mesh.visible = cache.get( mesh );
} else {
cache.set( mesh, mesh.visible );
mesh.visible = bVisible;
}
}
}
_changeVisibilityOfNonSelectedObjects( bVisible ) {
const visibilityCache = this._visibilityCache;
const selectionCache = this._selectionCache;
function VisibilityChangeCallBack( object ) {
if ( object.isPoints || object.isLine || object.isLine2 ) {
// the visibility of points and lines is always set to false in order to
// not affect the outline computation
if ( bVisible === true ) {
object.visible = visibilityCache.get( object ); // restore
} else {
visibilityCache.set( object, object.visible );
object.visible = bVisible;
}
} else if ( object.isMesh || object.isSprite ) {
// only meshes and sprites are supported by OutlinePass
if ( ! selectionCache.has( object ) ) {
const visibility = object.visible;
if ( bVisible === false || visibilityCache.get( object ) === true ) {
object.visible = bVisible;
}
visibilityCache.set( object, visibility );
}
}
}
this.renderScene.traverse( VisibilityChangeCallBack );
}
_updateTextureMatrix() {
this.textureMatrix.set( 0.5, 0.0, 0.0, 0.5,
0.0, 0.5, 0.0, 0.5,
0.0, 0.0, 0.5, 0.5,
0.0, 0.0, 0.0, 1.0 );
this.textureMatrix.multiply( this.renderCamera.projectionMatrix );
this.textureMatrix.multiply( this.renderCamera.matrixWorldInverse );
}
_getPrepareMaskMaterial() {
return new ShaderMaterial( {
@@ -438,7 +558,8 @@ class OutlinePass extends Pass {
},
vertexShader:
`#include <morphtarget_pars_vertex>
`#include <batching_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
varying vec4 projTexCoord;
@@ -447,6 +568,7 @@ class OutlinePass extends Pass {
void main() {
#include <batching_vertex>
#include <skinbase_vertex>
#include <begin_vertex>
#include <morphtarget_vertex>
@@ -489,7 +611,7 @@ class OutlinePass extends Pass {
}
getEdgeDetectionMaterial() {
_getEdgeDetectionMaterial() {
return new ShaderMaterial( {
@@ -536,7 +658,7 @@ class OutlinePass extends Pass {
}
getSeperableBlurMaterial( maxRadius ) {
_getSeparableBlurMaterial( maxRadius ) {
return new ShaderMaterial( {
@@ -593,7 +715,7 @@ class OutlinePass extends Pass {
}
getOverlayMaterial() {
_getOverlayMaterial() {
return new ShaderMaterial( {
@@ -651,4 +773,4 @@ class OutlinePass extends Pass {
OutlinePass.BlurDirectionX = new Vector2( 1.0, 0.0 );
OutlinePass.BlurDirectionY = new Vector2( 0.0, 1.0 );
export { OutlinePass };
export { OutlinePass };
@@ -7,39 +7,86 @@ import {
CineonToneMapping,
AgXToneMapping,
ACESFilmicToneMapping,
NeutralToneMapping,
CustomToneMapping,
SRGBTransfer
} from 'three';
import { Pass, FullScreenQuad } from './Pass.js';
import { OutputShader } from '../shaders/OutputShader.js';
/**
* This pass is responsible for including tone mapping and color space conversion
* into your pass chain. In most cases, this pass should be included at the end
* of each pass chain. If a pass requires sRGB input (e.g. like FXAA), the pass
* must follow `OutputPass` in the pass chain.
*
* The tone mapping and color space settings are extracted from the renderer.
*
* ```js
* const outputPass = new OutputPass();
* composer.addPass( outputPass );
* ```
*
* @augments Pass
* @three_import import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
*/
class OutputPass extends Pass {
/**
* Constructs a new output pass.
*/
constructor() {
super();
//
/**
* This flag indicates that this is an output pass.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isOutputPass = true;
const shader = OutputShader;
this.uniforms = UniformsUtils.clone( shader.uniforms );
/**
* The pass uniforms.
*
* @type {Object}
*/
this.uniforms = UniformsUtils.clone( OutputShader.uniforms );
/**
* The pass material.
*
* @type {RawShaderMaterial}
*/
this.material = new RawShaderMaterial( {
name: shader.name,
name: OutputShader.name,
uniforms: this.uniforms,
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader
vertexShader: OutputShader.vertexShader,
fragmentShader: OutputShader.fragmentShader
} );
this.fsQuad = new FullScreenQuad( this.material );
// internals
// internal cache
this._fsQuad = new FullScreenQuad( this.material );
this._outputColorSpace = null;
this._toneMapping = null;
}
/**
* Performs the output pass.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer/*, deltaTime, maskActive */ ) {
this.uniforms[ 'tDiffuse' ].value = readBuffer.texture;
@@ -61,6 +108,8 @@ class OutputPass extends Pass {
else if ( this._toneMapping === CineonToneMapping ) this.material.defines.CINEON_TONE_MAPPING = '';
else if ( this._toneMapping === ACESFilmicToneMapping ) this.material.defines.ACES_FILMIC_TONE_MAPPING = '';
else if ( this._toneMapping === AgXToneMapping ) this.material.defines.AGX_TONE_MAPPING = '';
else if ( this._toneMapping === NeutralToneMapping ) this.material.defines.NEUTRAL_TONE_MAPPING = '';
else if ( this._toneMapping === CustomToneMapping ) this.material.defines.CUSTOM_TONE_MAPPING = '';
this.material.needsUpdate = true;
@@ -71,22 +120,26 @@ class OutputPass extends Pass {
if ( this.renderToScreen === true ) {
renderer.setRenderTarget( null );
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
} else {
renderer.setRenderTarget( writeBuffer );
if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
}
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*/
dispose() {
this.material.dispose();
this.fsQuad.dispose();
this._fsQuad.dispose();
}
@@ -5,34 +5,98 @@ import {
Mesh
} from 'three';
/**
* Abstract base class for all post processing passes.
*
* This module is only relevant for post processing with {@link WebGLRenderer}.
*
* @abstract
* @three_import import { Pass } from 'three/addons/postprocessing/Pass.js';
*/
class Pass {
/**
* Constructs a new pass.
*/
constructor() {
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isPass = true;
// if set to true, the pass is processed by the composer
/**
* If set to `true`, the pass is processed by the composer.
*
* @type {boolean}
* @default true
*/
this.enabled = true;
// if set to true, the pass indicates to swap read and write buffer after rendering
/**
* If set to `true`, the pass indicates to swap read and write buffer after rendering.
*
* @type {boolean}
* @default true
*/
this.needsSwap = true;
// if set to true, the pass clears its buffer before rendering
/**
* If set to `true`, the pass clears its buffer before rendering
*
* @type {boolean}
* @default false
*/
this.clear = false;
// if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer.
/**
* If set to `true`, the result of the pass is rendered to screen. The last pass in the composers
* pass chain gets automatically rendered to screen, no matter how this property is configured.
*
* @type {boolean}
* @default false
*/
this.renderToScreen = false;
}
/**
* Sets the size of the pass.
*
* @abstract
* @param {number} width - The width to set.
* @param {number} height - The height to set.
*/
setSize( /* width, height */ ) {}
/**
* This method holds the render logic of a pass. It must be implemented in all derived classes.
*
* @abstract
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*
* @abstract
*/
dispose() {}
}
@@ -58,26 +122,58 @@ class FullscreenTriangleGeometry extends BufferGeometry {
const _geometry = new FullscreenTriangleGeometry();
/**
* This module is a helper for passes which need to render a full
* screen effect which is quite common in context of post processing.
*
* The intended usage is to reuse a single full screen quad for rendering
* subsequent passes by just reassigning the `material` reference.
*
* This module can only be used with {@link WebGLRenderer}.
*
* @augments Mesh
* @three_import import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
*/
class FullScreenQuad {
/**
* Constructs a new full screen quad.
*
* @param {?Material} material - The material to render te full screen quad with.
*/
constructor( material ) {
this._mesh = new Mesh( _geometry, material );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the instance is no longer used in your app.
*/
dispose() {
this._mesh.geometry.dispose();
}
/**
* Renders the full screen quad.
*
* @param {WebGLRenderer} renderer - The renderer.
*/
render( renderer ) {
renderer.render( this._mesh, _camera );
}
/**
* The quad's material.
*
* @type {?Material}
*/
get material() {
return this._mesh.material;
@@ -3,27 +3,121 @@ import {
} from 'three';
import { Pass } from './Pass.js';
/**
* This class represents a render pass. It takes a camera and a scene and produces
* a beauty pass for subsequent post processing effects.
*
* ```js
* const renderPass = new RenderPass( scene, camera );
* composer.addPass( renderPass );
* ```
*
* @augments Pass
* @three_import import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
*/
class RenderPass extends Pass {
/**
* Constructs a new render pass.
*
* @param {Scene} scene - The scene to render.
* @param {Camera} camera - The camera.
* @param {?Material} [overrideMaterial=null] - The override material. If set, this material is used
* for all objects in the scene.
* @param {?(number|Color|string)} [clearColor=null] - The clear color of the render pass.
* @param {?number} [clearAlpha=null] - The clear alpha of the render pass.
*/
constructor( scene, camera, overrideMaterial = null, clearColor = null, clearAlpha = null ) {
super();
/**
* The scene to render.
*
* @type {Scene}
*/
this.scene = scene;
/**
* The camera.
*
* @type {Camera}
*/
this.camera = camera;
/**
* The override material. If set, this material is used
* for all objects in the scene.
*
* @type {?Material}
* @default null
*/
this.overrideMaterial = overrideMaterial;
/**
* The clear color of the render pass.
*
* @type {?(number|Color|string)}
* @default null
*/
this.clearColor = clearColor;
/**
* The clear alpha of the render pass.
*
* @type {?number}
* @default null
*/
this.clearAlpha = clearAlpha;
/**
* Overwritten to perform a clear operation by default.
*
* @type {boolean}
* @default true
*/
this.clear = true;
/**
* If set to `true`, only the depth can be cleared when `clear` is to `false`.
*
* @type {boolean}
* @default false
*/
this.clearDepth = false;
/**
* Overwritten to disable the swap.
*
* @type {boolean}
* @default false
*/
this.needsSwap = false;
/**
* This flag indicates that this pass renders the scene itself.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isRenderPass = true;
this._oldClearColor = new Color();
}
/**
* Performs a beauty pass with the configured scene and camera.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
const oldAutoClear = renderer.autoClear;
@@ -42,7 +136,7 @@ class RenderPass extends Pass {
if ( this.clearColor !== null ) {
renderer.getClearColor( this._oldClearColor );
renderer.setClearColor( this.clearColor );
renderer.setClearColor( this.clearColor, renderer.getClearAlpha() );
}
File diff suppressed because one or more lines are too long
@@ -4,13 +4,54 @@ import {
} from 'three';
import { Pass, FullScreenQuad } from './Pass.js';
/**
* This pass can be used to create a post processing effect
* with a raw GLSL shader object. Useful for implementing custom
* effects.
*
* ```js
* const fxaaPass = new ShaderPass( FXAAShader );
* composer.addPass( fxaaPass );
* ```
*
* @augments Pass
* @three_import import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
*/
class ShaderPass extends Pass {
constructor( shader, textureID ) {
/**
* Constructs a new shader pass.
*
* @param {Object|ShaderMaterial} [shader] - A shader object holding vertex and fragment shader as well as
* defines and uniforms. It's also valid to pass a custom shader material.
* @param {string} [textureID='tDiffuse'] - The name of the texture uniform that should sample
* the read buffer.
*/
constructor( shader, textureID = 'tDiffuse' ) {
super();
this.textureID = ( textureID !== undefined ) ? textureID : 'tDiffuse';
/**
* The name of the texture uniform that should sample the read buffer.
*
* @type {string}
* @default 'tDiffuse'
*/
this.textureID = textureID;
/**
* The pass uniforms.
*
* @type {?Object}
*/
this.uniforms = null;
/**
* The pass material.
*
* @type {?ShaderMaterial}
*/
this.material = null;
if ( shader instanceof ShaderMaterial ) {
@@ -34,10 +75,23 @@ class ShaderPass extends Pass {
}
this.fsQuad = new FullScreenQuad( this.material );
// internals
this._fsQuad = new FullScreenQuad( this.material );
}
/**
* Performs the shader pass.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
if ( this.uniforms[ this.textureID ] ) {
@@ -46,29 +100,33 @@ class ShaderPass extends Pass {
}
this.fsQuad.material = this.material;
this._fsQuad.material = this.material;
if ( this.renderToScreen ) {
renderer.setRenderTarget( null );
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
} else {
renderer.setRenderTarget( writeBuffer );
// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
}
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*/
dispose() {
this.material.dispose();
this.fsQuad.dispose();
this._fsQuad.dispose();
}
@@ -14,28 +14,87 @@ import { CopyShader } from '../shaders/CopyShader.js';
import { LuminosityHighPassShader } from '../shaders/LuminosityHighPassShader.js';
/**
* UnrealBloomPass is inspired by the bloom pass of Unreal Engine. It creates a
* This pass is inspired by the bloom pass of Unreal Engine. It creates a
* mip map chain of bloom textures and blurs them with different radii. Because
* of the weighted combination of mips, and because larger blurs are done on
* higher mips, this effect provides good quality and performance.
*
* When using this pass, tone mapping must be enabled in the renderer settings.
*
* Reference:
* - https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/Bloom/
* - [Bloom in Unreal Engine](https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/Bloom/)
*
* ```js
* const resolution = new THREE.Vector2( window.innerWidth, window.innerHeight );
* const bloomPass = new UnrealBloomPass( resolution, 1.5, 0.4, 0.85 );
* composer.addPass( bloomPass );
* ```
*
* @augments Pass
* @three_import import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
*/
class UnrealBloomPass extends Pass {
constructor( resolution, strength, radius, threshold ) {
/**
* Constructs a new Unreal Bloom pass.
*
* @param {Vector2} [resolution] - The effect's resolution.
* @param {number} [strength=1] - The Bloom strength.
* @param {number} radius - The Bloom radius.
* @param {number} threshold - The luminance threshold limits which bright areas contribute to the Bloom effect.
*/
constructor( resolution, strength = 1, radius, threshold ) {
super();
this.strength = ( strength !== undefined ) ? strength : 1;
/**
* The Bloom strength.
*
* @type {number}
* @default 1
*/
this.strength = strength;
/**
* The Bloom radius. Must be in the range `[0,1]`.
*
* @type {number}
*/
this.radius = radius;
/**
* The luminance threshold limits which bright areas contribute to the Bloom effect.
*
* @type {number}
*/
this.threshold = threshold;
/**
* The effect's resolution.
*
* @type {Vector2}
* @default (256,256)
*/
this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 );
// create color only once here, reuse it later inside the render function
/**
* The effect's clear color
*
* @type {Color}
* @default (0,0,0)
*/
this.clearColor = new Color( 0, 0, 0 );
/**
* Overwritten to disable the swap.
*
* @type {boolean}
* @default false
*/
this.needsSwap = false;
// internals
// render targets
this.renderTargetsHorizontal = [];
this.renderTargetsVertical = [];
@@ -49,12 +108,12 @@ class UnrealBloomPass extends Pass {
for ( let i = 0; i < this.nMips; i ++ ) {
const renderTargetHorizonal = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
const renderTargetHorizontal = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
renderTargetHorizonal.texture.name = 'UnrealBloomPass.h' + i;
renderTargetHorizonal.texture.generateMipmaps = false;
renderTargetHorizontal.texture.name = 'UnrealBloomPass.h' + i;
renderTargetHorizontal.texture.generateMipmaps = false;
this.renderTargetsHorizontal.push( renderTargetHorizonal );
this.renderTargetsHorizontal.push( renderTargetHorizontal );
const renderTargetVertical = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
@@ -86,13 +145,15 @@ class UnrealBloomPass extends Pass {
// gaussian blur materials
this.separableBlurMaterials = [];
const kernelSizeArray = [ 3, 5, 7, 9, 11 ];
// These sizes have been changed to account for the altered coefficients-calculation to avoid blockiness,
// while retaining the same blur-strength. For details see https://github.com/mrdoob/three.js/pull/31528
const kernelSizeArray = [ 6, 10, 14, 18, 22 ];
resx = Math.round( this.resolution.x / 2 );
resy = Math.round( this.resolution.y / 2 );
for ( let i = 0; i < this.nMips; i ++ ) {
this.separableBlurMaterials.push( this.getSeperableBlurMaterial( kernelSizeArray[ i ] ) );
this.separableBlurMaterials.push( this._getSeparableBlurMaterial( kernelSizeArray[ i ] ) );
this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy );
@@ -104,7 +165,7 @@ class UnrealBloomPass extends Pass {
// composite material
this.compositeMaterial = this.getCompositeMaterial( this.nMips );
this.compositeMaterial = this._getCompositeMaterial( this.nMips );
this.compositeMaterial.uniforms[ 'blurTexture1' ].value = this.renderTargetsVertical[ 0 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture2' ].value = this.renderTargetsVertical[ 1 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture3' ].value = this.renderTargetsVertical[ 2 ].texture;
@@ -120,32 +181,32 @@ class UnrealBloomPass extends Pass {
// blend material
const copyShader = CopyShader;
this.copyUniforms = UniformsUtils.clone( copyShader.uniforms );
this.copyUniforms = UniformsUtils.clone( CopyShader.uniforms );
this.blendMaterial = new ShaderMaterial( {
uniforms: this.copyUniforms,
vertexShader: copyShader.vertexShader,
fragmentShader: copyShader.fragmentShader,
vertexShader: CopyShader.vertexShader,
fragmentShader: CopyShader.fragmentShader,
premultipliedAlpha: true,
blending: AdditiveBlending,
depthTest: false,
depthWrite: false,
transparent: true
} );
this.enabled = true;
this.needsSwap = false;
this._oldClearColor = new Color();
this.oldClearAlpha = 1;
this._oldClearAlpha = 1;
this.basic = new MeshBasicMaterial();
this._basic = new MeshBasicMaterial();
this.fsQuad = new FullScreenQuad( null );
this._fsQuad = new FullScreenQuad( null );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*/
dispose() {
for ( let i = 0; i < this.renderTargetsHorizontal.length; i ++ ) {
@@ -172,14 +233,20 @@ class UnrealBloomPass extends Pass {
this.compositeMaterial.dispose();
this.blendMaterial.dispose();
this.basic.dispose();
this._basic.dispose();
//
this.fsQuad.dispose();
this._fsQuad.dispose();
}
/**
* Sets the size of the pass.
*
* @param {number} width - The width to set.
* @param {number} height - The height to set.
*/
setSize( width, height ) {
let resx = Math.round( width / 2 );
@@ -201,10 +268,21 @@ class UnrealBloomPass extends Pass {
}
/**
* Performs the Bloom pass.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {
renderer.getClearColor( this._oldClearColor );
this.oldClearAlpha = renderer.getClearAlpha();
this._oldClearAlpha = renderer.getClearAlpha();
const oldAutoClear = renderer.autoClear;
renderer.autoClear = false;
@@ -216,12 +294,12 @@ class UnrealBloomPass extends Pass {
if ( this.renderToScreen ) {
this.fsQuad.material = this.basic;
this.basic.map = readBuffer.texture;
this._fsQuad.material = this._basic;
this._basic.map = readBuffer.texture;
renderer.setRenderTarget( null );
renderer.clear();
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
}
@@ -229,11 +307,11 @@ class UnrealBloomPass extends Pass {
this.highPassUniforms[ 'tDiffuse' ].value = readBuffer.texture;
this.highPassUniforms[ 'luminosityThreshold' ].value = this.threshold;
this.fsQuad.material = this.materialHighPassFilter;
this._fsQuad.material = this.materialHighPassFilter;
renderer.setRenderTarget( this.renderTargetBright );
renderer.clear();
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
// 2. Blur All the mips progressively
@@ -241,19 +319,19 @@ class UnrealBloomPass extends Pass {
for ( let i = 0; i < this.nMips; i ++ ) {
this.fsQuad.material = this.separableBlurMaterials[ i ];
this._fsQuad.material = this.separableBlurMaterials[ i ];
this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = inputRenderTarget.texture;
this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionX;
renderer.setRenderTarget( this.renderTargetsHorizontal[ i ] );
renderer.clear();
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = this.renderTargetsHorizontal[ i ].texture;
this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionY;
renderer.setRenderTarget( this.renderTargetsVertical[ i ] );
renderer.clear();
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
inputRenderTarget = this.renderTargetsVertical[ i ];
@@ -261,18 +339,18 @@ class UnrealBloomPass extends Pass {
// Composite All the mips
this.fsQuad.material = this.compositeMaterial;
this._fsQuad.material = this.compositeMaterial;
this.compositeMaterial.uniforms[ 'bloomStrength' ].value = this.strength;
this.compositeMaterial.uniforms[ 'bloomRadius' ].value = this.radius;
this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;
renderer.setRenderTarget( this.renderTargetsHorizontal[ 0 ] );
renderer.clear();
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
// Blend it additively over the input texture
this.fsQuad.material = this.blendMaterial;
this._fsQuad.material = this.blendMaterial;
this.copyUniforms[ 'tDiffuse' ].value = this.renderTargetsHorizontal[ 0 ].texture;
if ( maskActive ) renderer.state.buffers.stencil.setTest( true );
@@ -280,29 +358,32 @@ class UnrealBloomPass extends Pass {
if ( this.renderToScreen ) {
renderer.setRenderTarget( null );
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
} else {
renderer.setRenderTarget( readBuffer );
this.fsQuad.render( renderer );
this._fsQuad.render( renderer );
}
// Restore renderer settings
renderer.setClearColor( this._oldClearColor, this.oldClearAlpha );
renderer.setClearColor( this._oldClearColor, this._oldClearAlpha );
renderer.autoClear = oldAutoClear;
}
getSeperableBlurMaterial( kernelRadius ) {
// internals
_getSeparableBlurMaterial( kernelRadius ) {
const coefficients = [];
const sigma = kernelRadius / 3;
for ( let i = 0; i < kernelRadius; i ++ ) {
coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( kernelRadius * kernelRadius ) ) / kernelRadius );
coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( sigma * sigma ) ) / sigma );
}
@@ -319,40 +400,52 @@ class UnrealBloomPass extends Pass {
'gaussianCoefficients': { value: coefficients } // precomputed Gaussian coefficients
},
vertexShader:
`varying vec2 vUv;
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader:
`#include <common>
fragmentShader: /* glsl */`
#include <common>
varying vec2 vUv;
uniform sampler2D colorTexture;
uniform vec2 invSize;
uniform vec2 direction;
uniform float gaussianCoefficients[KERNEL_RADIUS];
void main() {
float weightSum = gaussianCoefficients[0];
vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum;
for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
float x = float(i);
for ( int i = 1; i < KERNEL_RADIUS; i ++ ) {
float x = float( i );
float w = gaussianCoefficients[i];
vec2 uvOffset = direction * invSize * x;
vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb;
vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb;
diffuseSum += (sample1 + sample2) * w;
weightSum += 2.0 * w;
diffuseSum += ( sample1 + sample2 ) * w;
}
gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
gl_FragColor = vec4( diffuseSum, 1.0 );
}`
} );
}
getCompositeMaterial( nMips ) {
_getCompositeMaterial( nMips ) {
return new ShaderMaterial( {
@@ -372,15 +465,21 @@ class UnrealBloomPass extends Pass {
'bloomRadius': { value: 0.0 }
},
vertexShader:
`varying vec2 vUv;
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader:
`varying vec2 vUv;
fragmentShader: /* glsl */`
varying vec2 vUv;
uniform sampler2D blurTexture1;
uniform sampler2D blurTexture2;
uniform sampler2D blurTexture3;
@@ -391,17 +490,27 @@ class UnrealBloomPass extends Pass {
uniform float bloomFactors[NUM_MIPS];
uniform vec3 bloomTintColors[NUM_MIPS];
float lerpBloomFactor(const in float factor) {
float lerpBloomFactor( const in float factor ) {
float mirrorFactor = 1.2 - factor;
return mix(factor, mirrorFactor, bloomRadius);
return mix( factor, mirrorFactor, bloomRadius );
}
void main() {
gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) +
lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) +
lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) +
lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) +
lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) );
// 3.0 for backwards compatibility with previous alpha-based intensity
vec3 bloom = 3.0 * bloomStrength * (
lerpBloomFactor( bloomFactors[ 0 ] ) * bloomTintColors[ 0 ] * texture2D( blurTexture1, vUv ).rgb +
lerpBloomFactor( bloomFactors[ 1 ] ) * bloomTintColors[ 1 ] * texture2D( blurTexture2, vUv ).rgb +
lerpBloomFactor( bloomFactors[ 2 ] ) * bloomTintColors[ 2 ] * texture2D( blurTexture3, vUv ).rgb +
lerpBloomFactor( bloomFactors[ 3 ] ) * bloomTintColors[ 3 ] * texture2D( blurTexture4, vUv ).rgb +
lerpBloomFactor( bloomFactors[ 4 ] ) * bloomTintColors[ 4 ] * texture2D( blurTexture5, vUv ).rgb
);
float bloomAlpha = max( bloom.r, max( bloom.g, bloom.b ) );
gl_FragColor = vec4( bloom, bloomAlpha );
}`
} );
@@ -1,10 +1,16 @@
/**
* Brightness and contrast adjustment
* https://github.com/evanw/glfx.js
* brightness: -1 to 1 (-1 is solid black, 0 is no change, and 1 is solid white)
* contrast: -1 to 1 (-1 is solid gray, 0 is no change, and 1 is maximum contrast)
* @module BrightnessContrastShader
* @three_import import { BrightnessContrastShader } from 'three/addons/shaders/BrightnessContrastShader.js';
*/
/**
* Brightness and contrast adjustment {@link https://github.com/evanw/glfx.js}.
* Brightness: -1 to 1 (-1 is solid black, 0 is no change, and 1 is solid white)
* Contrast: -1 to 1 (-1 is solid gray, 0 is no change, and 1 is maximum contrast)
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const BrightnessContrastShader = {
name: 'BrightnessContrastShader',
@@ -1,7 +1,14 @@
/**
* Full-screen textured quad shader
* @module CopyShader
* @three_import import { CopyShader } from 'three/addons/shaders/CopyShader.js';
*/
/**
* Full-screen copy shader pass.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const CopyShader = {
name: 'CopyShader',
@@ -1,7 +1,14 @@
/**
* Exposure shader
* @module ExposureShader
* @three_import import { ExposureShader } from 'three/addons/shaders/ExposureShader.js';
*/
/**
* TODO
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const ExposureShader = {
name: 'ExposureShader',
@@ -1,10 +1,17 @@
/**
* Hue and saturation adjustment
* https://github.com/evanw/glfx.js
* hue: -1 to 1 (-1 is 180 degrees in the negative direction, 0 is no change, etc.
* saturation: -1 to 1 (-1 is solid gray, 0 is no change, and 1 is maximum contrast)
* @module HueSaturationShader
* @three_import import { HueSaturationShader } from 'three/addons/shaders/HueSaturationShader.js';
*/
/**
* Hue and saturation adjustment, {@link https://github.com/evanw/glfx.js}.
*
* hue: -1 to 1 (-1 is 180 degrees in the negative direction, 0 is no change, etc.
* saturation: -1 to 1 (-1 is solid gray, 0 is no change, and 1 is maximum contrast)
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const HueSaturationShader = {
name: 'HueSaturationShader',
@@ -3,16 +3,20 @@ import {
} from 'three';
/**
* Luminosity
* http://en.wikipedia.org/wiki/Luminosity
* @module LuminosityHighPassShader
* @three_import import { LuminosityHighPassShader } from 'three/addons/shaders/LuminosityHighPassShader.js';
*/
/**
* Luminosity high pass shader.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const LuminosityHighPassShader = {
name: 'LuminosityHighPassShader',
shaderID: 'luminosityHighPass',
uniforms: {
'tDiffuse': { value: null },
@@ -49,9 +53,7 @@ const LuminosityHighPassShader = {
vec4 texel = texture2D( tDiffuse, vUv );
vec3 luma = vec3( 0.299, 0.587, 0.114 );
float v = dot( texel.xyz, luma );
float v = luminance( texel.xyz );
vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity );
@@ -1,3 +1,17 @@
/**
* @module OutputShader
* @three_import import { OutputShader } from 'three/addons/shaders/OutputShader.js';
*/
/**
* Performs tone mapping and color space conversion for
* FX workflows.
*
* Used by {@link OutputPass}.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const OutputShader = {
name: 'OutputShader',
@@ -28,7 +42,7 @@ const OutputShader = {
}`,
fragmentShader: /* glsl */`
precision highp float;
uniform sampler2D tDiffuse;
@@ -54,7 +68,7 @@ const OutputShader = {
#elif defined( CINEON_TONE_MAPPING )
gl_FragColor.rgb = OptimizedCineonToneMapping( gl_FragColor.rgb );
gl_FragColor.rgb = CineonToneMapping( gl_FragColor.rgb );
#elif defined( ACES_FILMIC_TONE_MAPPING )
@@ -64,6 +78,14 @@ const OutputShader = {
gl_FragColor.rgb = AgXToneMapping( gl_FragColor.rgb );
#elif defined( NEUTRAL_TONE_MAPPING )
gl_FragColor.rgb = NeutralToneMapping( gl_FragColor.rgb );
#elif defined( CUSTOM_TONE_MAPPING )
gl_FragColor.rgb = CustomToneMapping( gl_FragColor.rgb );
#endif
// color space
@@ -5,9 +5,20 @@ import {
/**
* WebGL port of Subpixel Morphological Antialiasing (SMAA) v2.8
* Preset: SMAA 1x Medium (with color edge detection)
* https://github.com/iryoku/smaa/releases/tag/v2.8
*
* References:
* - {@link https://github.com/iryoku/smaa/releases/tag/v2.8}
*
* @module SMAAShader
* @three_import import { SMAAShader } from 'three/addons/shaders/SMAAShader.js';
*/
/**
* SMAA Edges shader.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const SMAAEdgesShader = {
name: 'SMAAEdgesShader',
@@ -115,6 +126,12 @@ const SMAAEdgesShader = {
};
/**
* SMAA Weights shader.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const SMAAWeightsShader = {
name: 'SMAAWeightsShader',
@@ -371,6 +388,12 @@ const SMAAWeightsShader = {
};
/**
* SMAA Blend shader.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const SMAABlendShader = {
name: 'SMAABlendShader',
@@ -11,17 +11,41 @@ import {
Vector3,
} from 'three';
/**
* @module BufferGeometryUtils
* @three_import import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
*/
/**
* Computes vertex tangents using the MikkTSpace algorithm. MikkTSpace generates the same tangents consistently,
* and is used in most modelling tools and normal map bakers. Use MikkTSpace for materials with normal maps,
* because inconsistent tangents may lead to subtle visual issues in the normal map, particularly around mirrored
* UV seams.
*
* In comparison to this method, {@link BufferGeometry#computeTangents} (a custom algorithm) generates tangents that
* probably will not match the tangents in other software. The custom algorithm is sufficient for general use with a
* custom material, and may be faster than MikkTSpace.
*
* Returns the original BufferGeometry. Indexed geometries will be de-indexed. Requires position, normal, and uv attributes.
*
* @param {BufferGeometry} geometry - The geometry to compute tangents for.
* @param {Object} MikkTSpace - Instance of `examples/jsm/libs/mikktspace.module.js`, or `mikktspace` npm package.
* Await `MikkTSpace.ready` before use.
* @param {boolean} [negateSign=true] - Whether to negate the sign component (.w) of each tangent.
* Required for normal map conventions in some formats, including glTF.
* @return {BufferGeometry} The updated geometry.
*/
function computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) {
if ( ! MikkTSpace || ! MikkTSpace.isReady ) {
throw new Error( 'BufferGeometryUtils: Initialized MikkTSpace library required.' );
throw new Error( 'THREE.BufferGeometryUtils: Initialized MikkTSpace library required.' );
}
if ( ! geometry.hasAttribute( 'position' ) || ! geometry.hasAttribute( 'normal' ) || ! geometry.hasAttribute( 'uv' ) ) {
throw new Error( 'BufferGeometryUtils: Tangents require "position", "normal", and "uv" attributes.' );
throw new Error( 'THREE.BufferGeometryUtils: Tangents require "position", "normal", and "uv" attributes.' );
}
@@ -100,9 +124,11 @@ function computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) {
}
/**
* @param {Array<BufferGeometry>} geometries
* @param {Boolean} useGroups
* @return {BufferGeometry}
* Merges a set of geometries into a single instance. All geometries must have compatible attributes.
*
* @param {Array<BufferGeometry>} geometries - The geometries to merge.
* @param {boolean} [useGroups=false] - Whether to use groups or not.
* @return {?BufferGeometry} The merged geometry. Returns `null` if the merge does not succeed.
*/
function mergeGeometries( geometries, useGroups = false ) {
@@ -260,8 +286,7 @@ function mergeGeometries( geometries, useGroups = false ) {
for ( const name in morphAttributes ) {
const numMorphTargets = morphAttributes[ name ][ 0 ].length;
if ( numMorphTargets === 0 ) break;
if ( numMorphTargets === 0 ) continue;
mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {};
mergedGeometry.morphAttributes[ name ] = [];
@@ -296,8 +321,11 @@ function mergeGeometries( geometries, useGroups = false ) {
}
/**
* @param {Array<BufferAttribute>} attributes
* @return {BufferAttribute}
* Merges a set of attributes into a single instance. All attributes must have compatible properties and types.
* Instances of {@link InterleavedBufferAttribute} are not supported.
*
* @param {Array<BufferAttribute>} attributes - The attributes to merge.
* @return {?BufferAttribute} The merged attribute. Returns `null` if the merge does not succeed.
*/
function mergeAttributes( attributes ) {
@@ -311,13 +339,6 @@ function mergeAttributes( attributes ) {
const attribute = attributes[ i ];
if ( attribute.isInterleavedBufferAttribute ) {
console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. InterleavedBufferAttributes are not supported.' );
return null;
}
if ( TypedArray === undefined ) TypedArray = attribute.array.constructor;
if ( TypedArray !== attribute.array.constructor ) {
@@ -350,22 +371,41 @@ function mergeAttributes( attributes ) {
}
arrayLength += attribute.array.length;
arrayLength += attribute.count * itemSize;
}
const array = new TypedArray( arrayLength );
const result = new BufferAttribute( array, itemSize, normalized );
let offset = 0;
for ( let i = 0; i < attributes.length; ++ i ) {
array.set( attributes[ i ].array, offset );
const attribute = attributes[ i ];
if ( attribute.isInterleavedBufferAttribute ) {
offset += attributes[ i ].array.length;
const tupleOffset = offset / itemSize;
for ( let j = 0, l = attribute.count; j < l; j ++ ) {
for ( let c = 0; c < itemSize; c ++ ) {
const value = attribute.getComponent( j, c );
result.setComponent( j + tupleOffset, c, value );
}
}
} else {
array.set( attribute.array, offset );
}
offset += attribute.count * itemSize;
}
const result = new BufferAttribute( array, itemSize, normalized );
if ( gpuType !== undefined ) {
result.gpuType = gpuType;
@@ -377,10 +417,12 @@ function mergeAttributes( attributes ) {
}
/**
* @param {BufferAttribute}
* @return {BufferAttribute}
* Performs a deep clone of the given buffer attribute.
*
* @param {BufferAttribute} attribute - The attribute to clone.
* @return {BufferAttribute} The cloned attribute.
*/
export function deepCloneAttribute( attribute ) {
function deepCloneAttribute( attribute ) {
if ( attribute.isInstancedInterleavedBufferAttribute || attribute.isInterleavedBufferAttribute ) {
@@ -399,8 +441,11 @@ export function deepCloneAttribute( attribute ) {
}
/**
* @param {Array<BufferAttribute>} attributes
* @return {Array<InterleavedBufferAttribute>}
* Interleaves a set of attributes and returns a new array of corresponding attributes that share a
* single {@link InterleavedBuffer} instance. All attributes must have compatible types.
*
* @param {Array<BufferAttribute>} attributes - The attributes to interleave.
* @return {?Array<InterleavedBufferAttribute>} An array of interleaved attributes. If interleave does not succeed, the method returns `null`.
*/
function interleaveAttributes( attributes ) {
@@ -463,8 +508,13 @@ function interleaveAttributes( attributes ) {
}
// returns a new, non-interleaved version of the provided attribute
export function deinterleaveAttribute( attribute ) {
/**
* Returns a new, non-interleaved version of the given attribute.
*
* @param {InterleavedBufferAttribute} attribute - The interleaved attribute.
* @return {BufferAttribute} The non-interleaved attribute.
*/
function deinterleaveAttribute( attribute ) {
const cons = attribute.data.array.constructor;
const count = attribute.count;
@@ -511,8 +561,12 @@ export function deinterleaveAttribute( attribute ) {
}
// deinterleaves all attributes on the geometry
export function deinterleaveGeometry( geometry ) {
/**
* Deinterleaves all attributes on the given geometry.
*
* @param {BufferGeometry} geometry - The geometry to deinterleave.
*/
function deinterleaveGeometry( geometry ) {
const attributes = geometry.attributes;
const morphTargets = geometry.morphTargets;
@@ -555,8 +609,10 @@ export function deinterleaveGeometry( geometry ) {
}
/**
* @param {BufferGeometry} geometry
* @return {number}
* Returns the amount of bytes used by all attributes to represent the geometry.
*
* @param {BufferGeometry} geometry - The geometry.
* @return {number} The estimate bytes used.
*/
function estimateBytesUsed( geometry ) {
@@ -578,9 +634,11 @@ function estimateBytesUsed( geometry ) {
}
/**
* @param {BufferGeometry} geometry
* @param {number} tolerance
* @return {BufferGeometry}
* Returns a new geometry with vertices for which all similar vertex attributes (within tolerance) are merged.
*
* @param {BufferGeometry} geometry - The geometry to merge vertices for.
* @param {number} [tolerance=1e-4] - The tolerance value.
* @return {BufferGeometry} - The new geometry with merged vertices.
*/
function mergeVertices( geometry, tolerance = 1e-4 ) {
@@ -611,20 +669,22 @@ function mergeVertices( geometry, tolerance = 1e-4 ) {
const name = attributeNames[ i ];
const attr = geometry.attributes[ name ];
tmpAttributes[ name ] = new BufferAttribute(
tmpAttributes[ name ] = new attr.constructor(
new attr.array.constructor( attr.count * attr.itemSize ),
attr.itemSize,
attr.normalized
);
const morphAttr = geometry.morphAttributes[ name ];
if ( morphAttr ) {
const morphAttributes = geometry.morphAttributes[ name ];
if ( morphAttributes ) {
tmpMorphAttributes[ name ] = new BufferAttribute(
new morphAttr.array.constructor( morphAttr.count * morphAttr.itemSize ),
morphAttr.itemSize,
morphAttr.normalized
);
if ( ! tmpMorphAttributes[ name ] ) tmpMorphAttributes[ name ] = [];
morphAttributes.forEach( ( morphAttr, i ) => {
const array = new morphAttr.array.constructor( morphAttr.count * morphAttr.itemSize );
tmpMorphAttributes[ name ][ i ] = new morphAttr.constructor( array, morphAttr.itemSize, morphAttr.normalized );
} );
}
@@ -669,22 +729,22 @@ function mergeVertices( geometry, tolerance = 1e-4 ) {
const name = attributeNames[ j ];
const attribute = geometry.getAttribute( name );
const morphAttr = geometry.morphAttributes[ name ];
const morphAttributes = geometry.morphAttributes[ name ];
const itemSize = attribute.itemSize;
const newarray = tmpAttributes[ name ];
const newArray = tmpAttributes[ name ];
const newMorphArrays = tmpMorphAttributes[ name ];
for ( let k = 0; k < itemSize; k ++ ) {
const getterFunc = getters[ k ];
const setterFunc = setters[ k ];
newarray[ setterFunc ]( nextIndex, attribute[ getterFunc ]( index ) );
newArray[ setterFunc ]( nextIndex, attribute[ getterFunc ]( index ) );
if ( morphAttr ) {
if ( morphAttributes ) {
for ( let m = 0, ml = morphAttr.length; m < ml; m ++ ) {
for ( let m = 0, ml = morphAttributes.length; m < ml; m ++ ) {
newMorphArrays[ m ][ setterFunc ]( nextIndex, morphAttr[ m ][ getterFunc ]( index ) );
newMorphArrays[ m ][ setterFunc ]( nextIndex, morphAttributes[ m ][ getterFunc ]( index ) );
}
@@ -708,7 +768,7 @@ function mergeVertices( geometry, tolerance = 1e-4 ) {
const tmpAttribute = tmpAttributes[ name ];
result.setAttribute( name, new BufferAttribute(
result.setAttribute( name, new tmpAttribute.constructor(
tmpAttribute.array.slice( 0, nextIndex * tmpAttribute.itemSize ),
tmpAttribute.itemSize,
tmpAttribute.normalized,
@@ -720,7 +780,7 @@ function mergeVertices( geometry, tolerance = 1e-4 ) {
const tmpMorphAttribute = tmpMorphAttributes[ name ][ j ];
result.morphAttributes[ name ][ j ] = new BufferAttribute(
result.morphAttributes[ name ][ j ] = new tmpMorphAttribute.constructor(
tmpMorphAttribute.array.slice( 0, nextIndex * tmpMorphAttribute.itemSize ),
tmpMorphAttribute.itemSize,
tmpMorphAttribute.normalized,
@@ -739,9 +799,12 @@ function mergeVertices( geometry, tolerance = 1e-4 ) {
}
/**
* @param {BufferGeometry} geometry
* @param {number} drawMode
* @return {BufferGeometry}
* Returns a new indexed geometry based on `TrianglesDrawMode` draw mode.
* This mode corresponds to the `gl.TRIANGLES` primitive in WebGL.
*
* @param {BufferGeometry} geometry - The geometry to convert.
* @param {number} drawMode - The current draw mode.
* @return {BufferGeometry} The new geometry using `TrianglesDrawMode`.
*/
function toTrianglesDrawMode( geometry, drawMode ) {
@@ -850,9 +913,13 @@ function toTrianglesDrawMode( geometry, drawMode ) {
/**
* Calculates the morphed attributes of a morphed/skinned BufferGeometry.
* Helpful for Raytracing or Decals.
* @param {Mesh | Line | Points} object An instance of Mesh, Line or Points.
* @return {Object} An Object with original position/normal attributes and morphed ones.
*
* Helpful for Raytracing or Decals (i.e. a `DecalGeometry` applied to a morphed Object with a `BufferGeometry`
* will use the original `BufferGeometry`, not the morphed/skinned one, generating an incorrect result.
* Using this function to create a shadow `Object3`D the `DecalGeometry` can be correctly generated).
*
* @param {Mesh|Line|Points} object - The 3D object to compute morph attributes for.
* @return {Object} An object with original position/normal attributes and morphed ones.
*/
function computeMorphedAttributes( object ) {
@@ -953,7 +1020,7 @@ function computeMorphedAttributes( object ) {
const morphPosition = geometry.morphAttributes.position;
const morphTargetsRelative = geometry.morphTargetsRelative;
const normalAttribute = geometry.attributes.normal;
const morphNormal = geometry.morphAttributes.position;
const morphNormal = geometry.morphAttributes.normal;
const groups = geometry.groups;
const drawRange = geometry.drawRange;
@@ -1128,6 +1195,12 @@ function computeMorphedAttributes( object ) {
}
/**
* Merges the {@link BufferGeometry#groups} for the given geometry.
*
* @param {BufferGeometry} geometry - The geometry to modify.
* @return {BufferGeometry} - The updated geometry
*/
function mergeGroups( geometry ) {
if ( geometry.groups.length === 0 ) {
@@ -1230,141 +1303,194 @@ function mergeGroups( geometry ) {
}
/**
* Modifies the supplied geometry if it is non-indexed, otherwise creates a new,
* non-indexed geometry. Returns the geometry with smooth normals everywhere except
* faces that meet at an angle greater than the crease angle.
*
* @param {BufferGeometry} geometry
* @param {number} [creaseAngle]
* @return {BufferGeometry}
* @param {BufferGeometry} geometry - The geometry to modify.
* @param {number} [creaseAngle=Math.PI/3] - The crease angle in radians.
* @return {BufferGeometry} - The updated geometry
*/
function toCreasedNormals( geometry, creaseAngle = Math.PI / 3 /* 60 degrees */ ) {
const creaseDot = Math.cos( creaseAngle );
const hashMultiplier = ( 1 + 1e-10 ) * 1e2;
// reusable vectors
const verts = [ new Vector3(), new Vector3(), new Vector3() ];
const tempVec1 = new Vector3();
const tempVec2 = new Vector3();
const tempNorm = new Vector3();
const tempNorm2 = new Vector3();
// hashes a vector
function hashVertex( v ) {
const x = ~ ~ ( v.x * hashMultiplier );
const y = ~ ~ ( v.y * hashMultiplier );
const z = ~ ~ ( v.z * hashMultiplier );
return `${x},${y},${z}`;
}
// BufferGeometry.toNonIndexed() warns if the geometry is non-indexed
// and returns the original geometry
const resultGeometry = geometry.index ? geometry.toNonIndexed() : geometry;
const posAttr = resultGeometry.attributes.position;
const vertexMap = {};
const vertexCount = posAttr.count;
// find all the normals shared by commonly located vertices
for ( let i = 0, l = posAttr.count / 3; i < l; i ++ ) {
let positions;
const i3 = 3 * i;
const a = verts[ 0 ].fromBufferAttribute( posAttr, i3 + 0 );
const b = verts[ 1 ].fromBufferAttribute( posAttr, i3 + 1 );
const c = verts[ 2 ].fromBufferAttribute( posAttr, i3 + 2 );
if ( posAttr.isBufferAttribute === true && posAttr.itemSize === 3 && posAttr.normalized === false ) {
tempVec1.subVectors( c, b );
tempVec2.subVectors( a, b );
positions = posAttr.array;
// add the normal to the map for all vertices
const normal = new Vector3().crossVectors( tempVec1, tempVec2 ).normalize();
for ( let n = 0; n < 3; n ++ ) {
} else {
const vert = verts[ n ];
const hash = hashVertex( vert );
if ( ! ( hash in vertexMap ) ) {
// flatten the position buffer so the math below operates on plain numbers
positions = new Float64Array( vertexCount * 3 );
vertexMap[ hash ] = [];
for ( let i = 0; i < vertexCount; i ++ ) {
}
vertexMap[ hash ].push( normal );
positions[ 3 * i + 0 ] = posAttr.getX( i );
positions[ 3 * i + 1 ] = posAttr.getY( i );
positions[ 3 * i + 2 ] = posAttr.getZ( i );
}
}
// average normals from all vertices that share a common location if they are within the
// provided crease threshold
const normalArray = new Float32Array( posAttr.count * 3 );
const normAttr = new BufferAttribute( normalArray, 3, false );
for ( let i = 0, l = posAttr.count / 3; i < l; i ++ ) {
const creaseDot = Math.cos( creaseAngle );
const hashMultiplier = ( 1 + 1e-10 ) * 1e2;
const faceCount = vertexCount / 3;
// compute the normal of each face
const faceNormals = new Float64Array( faceCount * 3 );
for ( let f = 0; f < faceCount; f ++ ) {
const f9 = 9 * f;
const ax = positions[ f9 + 0 ], ay = positions[ f9 + 1 ], az = positions[ f9 + 2 ];
const bx = positions[ f9 + 3 ], by = positions[ f9 + 4 ], bz = positions[ f9 + 5 ];
const cx = positions[ f9 + 6 ], cy = positions[ f9 + 7 ], cz = positions[ f9 + 8 ];
const v1x = cx - bx, v1y = cy - by, v1z = cz - bz;
const v2x = ax - bx, v2y = ay - by, v2z = az - bz;
const nx = v1y * v2z - v1z * v2y;
const ny = v1z * v2x - v1x * v2z;
const nz = v1x * v2y - v1y * v2x;
const invLength = 1 / ( Math.sqrt( nx * nx + ny * ny + nz * nz ) || 1 );
faceNormals[ 3 * f + 0 ] = nx * invLength;
faceNormals[ 3 * f + 1 ] = ny * invLength;
faceNormals[ 3 * f + 2 ] = nz * invLength;
}
// assign an id to each vertex, sharing the id between vertices with the same
// quantized position via an open-addressed hash table (slots hold id + 1, 0 means empty)
const vertexIds = new Int32Array( vertexCount );
const quantized = new Int32Array( vertexCount * 3 );
let tableSize = 1;
while ( tableSize < vertexCount * 2 ) tableSize <<= 1;
const tableMask = tableSize - 1;
const table = new Int32Array( tableSize );
let uniqueCount = 0;
for ( let i = 0; i < vertexCount; i ++ ) {
// get the face normal for this vertex
const i3 = 3 * i;
const a = verts[ 0 ].fromBufferAttribute( posAttr, i3 + 0 );
const b = verts[ 1 ].fromBufferAttribute( posAttr, i3 + 1 );
const c = verts[ 2 ].fromBufferAttribute( posAttr, i3 + 2 );
const qx = ~ ~ ( positions[ i3 + 0 ] * hashMultiplier );
const qy = ~ ~ ( positions[ i3 + 1 ] * hashMultiplier );
const qz = ~ ~ ( positions[ i3 + 2 ] * hashMultiplier );
tempVec1.subVectors( c, b );
tempVec2.subVectors( a, b );
let slot = ( Math.imul( qx, 73856093 ) ^ Math.imul( qy, 19349663 ) ^ Math.imul( qz, 83492791 ) ) & tableMask;
tempNorm.crossVectors( tempVec1, tempVec2 ).normalize();
while ( true ) {
const id = table[ slot ];
if ( id === 0 ) {
const q3 = 3 * uniqueCount;
quantized[ q3 + 0 ] = qx;
quantized[ q3 + 1 ] = qy;
quantized[ q3 + 2 ] = qz;
table[ slot ] = uniqueCount + 1;
vertexIds[ i ] = uniqueCount ++;
break;
}
const q3 = 3 * ( id - 1 );
if ( quantized[ q3 + 0 ] === qx && quantized[ q3 + 1 ] === qy && quantized[ q3 + 2 ] === qz ) {
vertexIds[ i ] = id - 1;
break;
}
slot = ( slot + 1 ) & tableMask;
}
}
// bucket the faces surrounding each unique vertex position
const bucketOffsets = new Int32Array( uniqueCount + 1 );
for ( let i = 0; i < vertexCount; i ++ ) bucketOffsets[ vertexIds[ i ] + 1 ] ++;
for ( let i = 0; i < uniqueCount; i ++ ) bucketOffsets[ i + 1 ] += bucketOffsets[ i ];
const bucketFaces = new Int32Array( vertexCount );
const bucketCursors = bucketOffsets.slice( 0, uniqueCount );
for ( let f = 0; f < faceCount; f ++ ) {
const f3 = 3 * f;
bucketFaces[ bucketCursors[ vertexIds[ f3 + 0 ] ] ++ ] = f;
bucketFaces[ bucketCursors[ vertexIds[ f3 + 1 ] ] ++ ] = f;
bucketFaces[ bucketCursors[ vertexIds[ f3 + 2 ] ] ++ ] = f;
}
// average the normals of the faces surrounding each vertex if they are within the
// provided crease threshold
const normalArray = new Float32Array( vertexCount * 3 );
for ( let f = 0; f < faceCount; f ++ ) {
const f3 = 3 * f;
const nx = faceNormals[ f3 + 0 ];
const ny = faceNormals[ f3 + 1 ];
const nz = faceNormals[ f3 + 2 ];
// average all normals that meet the threshold and set the normal value
for ( let n = 0; n < 3; n ++ ) {
const vert = verts[ n ];
const hash = hashVertex( vert );
const otherNormals = vertexMap[ hash ];
tempNorm2.set( 0, 0, 0 );
const i = f3 + n;
const id = vertexIds[ i ];
for ( let k = 0, lk = otherNormals.length; k < lk; k ++ ) {
let sumX = 0, sumY = 0, sumZ = 0;
const otherNorm = otherNormals[ k ];
if ( tempNorm.dot( otherNorm ) > creaseDot ) {
for ( let k = bucketOffsets[ id ], end = bucketOffsets[ id + 1 ]; k < end; k ++ ) {
tempNorm2.add( otherNorm );
const o3 = 3 * bucketFaces[ k ];
const ox = faceNormals[ o3 + 0 ];
const oy = faceNormals[ o3 + 1 ];
const oz = faceNormals[ o3 + 2 ];
if ( nx * ox + ny * oy + nz * oz > creaseDot ) {
sumX += ox;
sumY += oy;
sumZ += oz;
}
}
tempNorm2.normalize();
normAttr.setXYZ( i3 + n, tempNorm2.x, tempNorm2.y, tempNorm2.z );
const invLength = 1 / ( Math.sqrt( sumX * sumX + sumY * sumY + sumZ * sumZ ) || 1 );
normalArray[ 3 * i + 0 ] = sumX * invLength;
normalArray[ 3 * i + 1 ] = sumY * invLength;
normalArray[ 3 * i + 2 ] = sumZ * invLength;
}
}
resultGeometry.setAttribute( 'normal', normAttr );
resultGeometry.setAttribute( 'normal', new BufferAttribute( normalArray, 3, false ) );
return resultGeometry;
}
function mergeBufferGeometries( geometries, useGroups = false ) {
console.warn( 'THREE.BufferGeometryUtils: mergeBufferGeometries() has been renamed to mergeGeometries().' ); // @deprecated, r151
return mergeGeometries( geometries, useGroups );
}
function mergeBufferAttributes( attributes ) {
console.warn( 'THREE.BufferGeometryUtils: mergeBufferAttributes() has been renamed to mergeAttributes().' ); // @deprecated, r151
return mergeAttributes( attributes );
}
export {
computeMikkTSpaceTangents,
mergeGeometries,
mergeBufferGeometries,
mergeAttributes,
mergeBufferAttributes,
deepCloneAttribute,
deinterleaveAttribute,
deinterleaveGeometry,
interleaveAttributes,
estimateBytesUsed,
mergeVertices,
@@ -9,28 +9,52 @@ import {
VectorKeyframeTrack
} from 'three';
/**
* @module SkeletonUtils
* @three_import import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
*/
function getBoneName( bone, options ) {
if ( options.getBoneName !== undefined ) {
return options.getBoneName( bone );
}
return options.names[ bone.name ];
}
/**
* Retargets the skeleton from the given source to the target.
*
* Both `target` and `source` can be a 3D object with a skeleton property (e.g. a skinned mesh)
* or a {@link Skeleton} directly.
*
* @param {Object3D|Skeleton} target - The target object.
* @param {Object3D|Skeleton} source - The source object.
* @param {module:SkeletonUtils~RetargetOptions} options - The options.
*/
function retarget( target, source, options = {} ) {
const pos = new Vector3(),
quat = new Quaternion(),
const quat = new Quaternion(),
scale = new Vector3(),
bindBoneMatrix = new Matrix4(),
relativeMatrix = new Matrix4(),
globalMatrix = new Matrix4();
options.preserveMatrix = options.preserveMatrix !== undefined ? options.preserveMatrix : true;
options.preservePosition = options.preservePosition !== undefined ? options.preservePosition : true;
options.preserveHipPosition = options.preserveHipPosition !== undefined ? options.preserveHipPosition : false;
options.preserveBoneMatrix = options.preserveBoneMatrix !== undefined ? options.preserveBoneMatrix : true;
options.preserveBonePositions = options.preserveBonePositions !== undefined ? options.preserveBonePositions : true;
options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false;
options.hip = options.hip !== undefined ? options.hip : 'hip';
options.hipInfluence = options.hipInfluence !== undefined ? options.hipInfluence : new Vector3( 1, 1, 1 );
options.scale = options.scale !== undefined ? options.scale : 1;
options.names = options.names || {};
const sourceBones = source.isObject3D ? source.skeleton.bones : getBones( source ),
bones = target.isObject3D ? target.skeleton.bones : getBones( target );
let bindBones,
bone, name, boneTo,
let bone, name, boneTo,
bonesPosition;
// reset bones
@@ -42,11 +66,11 @@ function retarget( target, source, options = {} ) {
} else {
options.useTargetMatrix = true;
options.preserveMatrix = false;
options.preserveBoneMatrix = false;
}
if ( options.preservePosition ) {
if ( options.preserveBonePositions ) {
bonesPosition = [];
@@ -58,7 +82,7 @@ function retarget( target, source, options = {} ) {
}
if ( options.preserveMatrix ) {
if ( options.preserveBoneMatrix ) {
// reset matrix
@@ -76,35 +100,10 @@ function retarget( target, source, options = {} ) {
}
if ( options.offsets ) {
bindBones = [];
for ( let i = 0; i < bones.length; ++ i ) {
bone = bones[ i ];
name = options.names[ bone.name ] || bone.name;
if ( options.offsets[ name ] ) {
bone.matrix.multiply( options.offsets[ name ] );
bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
bone.updateMatrixWorld();
}
bindBones.push( bone.matrixWorld.clone() );
}
}
for ( let i = 0; i < bones.length; ++ i ) {
bone = bones[ i ];
name = options.names[ bone.name ] || bone.name;
name = getBoneName( bone, options );
boneTo = getBoneByName( name, sourceBones );
@@ -136,10 +135,15 @@ function retarget( target, source, options = {} ) {
if ( target.isObject3D ) {
const boneIndex = bones.indexOf( bone ),
wBindMatrix = bindBones ? bindBones[ boneIndex ] : bindBoneMatrix.copy( target.skeleton.boneInverses[ boneIndex ] ).invert();
if ( options.localOffsets ) {
globalMatrix.multiply( wBindMatrix );
if ( options.localOffsets[ bone.name ] ) {
globalMatrix.multiply( options.localOffsets[ bone.name ] );
}
}
}
@@ -147,7 +151,23 @@ function retarget( target, source, options = {} ) {
}
if ( bone.parent && bone.parent.isBone ) {
if ( name === options.hip ) {
globalMatrix.elements[ 12 ] *= options.scale * options.hipInfluence.x;
globalMatrix.elements[ 13 ] *= options.scale * options.hipInfluence.y;
globalMatrix.elements[ 14 ] *= options.scale * options.hipInfluence.z;
if ( options.hipPosition !== undefined ) {
globalMatrix.elements[ 12 ] += options.hipPosition.x * options.scale;
globalMatrix.elements[ 13 ] += options.hipPosition.y * options.scale;
globalMatrix.elements[ 14 ] += options.hipPosition.z * options.scale;
}
}
if ( bone.parent ) {
bone.matrix.copy( bone.parent.matrixWorld ).invert();
bone.matrix.multiply( globalMatrix );
@@ -158,24 +178,18 @@ function retarget( target, source, options = {} ) {
}
if ( options.preserveHipPosition && name === options.hip ) {
bone.matrix.setPosition( pos.set( 0, bone.position.y, 0 ) );
}
bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
bone.updateMatrixWorld();
}
if ( options.preservePosition ) {
if ( options.preserveBonePositions ) {
for ( let i = 0; i < bones.length; ++ i ) {
bone = bones[ i ];
name = options.names[ bone.name ] || bone.name;
name = getBoneName( bone, options ) || bone.name;
if ( name !== options.hip ) {
@@ -187,7 +201,7 @@ function retarget( target, source, options = {} ) {
}
if ( options.preserveMatrix ) {
if ( options.preserveBoneMatrix ) {
// restore matrix
@@ -197,10 +211,24 @@ function retarget( target, source, options = {} ) {
}
/**
* Retargets the animation clip of the source to the target 3D object.
*
* The `source` can be a 3D object with a skeleton property (e.g. a skinned mesh)
* or a {@link Skeleton} directly.
*
* @param {Object3D} target - The target 3D object. Must have a `skeleton` property.
* @param {Object3D|Skeleton} source - The source object.
* @param {AnimationClip} clip - The animation clip.
* @param {module:SkeletonUtils~RetargetOptions} options - The options.
* @return {AnimationClip} The retargeted animation clip.
*/
function retargetClip( target, source, clip, options = {} ) {
options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false;
options.fps = options.fps !== undefined ? options.fps : 30;
// Calculate the fps from the source clip based on the track with the most frames, unless fps is already provided.
options.fps = options.fps !== undefined ? options.fps : ( Math.max( ...clip.tracks.map( track => track.times.length ) ) / clip.duration );
options.names = options.names || [];
if ( ! source.isObject3D ) {
@@ -210,35 +238,53 @@ function retargetClip( target, source, clip, options = {} ) {
}
const numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ),
delta = 1 / options.fps,
delta = clip.duration / ( numFrames - 1 ),
convertedTracks = [],
mixer = new AnimationMixer( source ),
bones = getBones( target.skeleton ),
boneDatas = [];
let positionOffset,
bone, boneTo, boneData,
name;
mixer.clipAction( clip ).play();
mixer.update( 0 );
// trim
let start = 0, end = numFrames;
if ( options.trim !== undefined ) {
start = Math.round( options.trim[ 0 ] * options.fps );
end = Math.min( Math.round( options.trim[ 1 ] * options.fps ), numFrames ) - start;
mixer.update( options.trim[ 0 ] );
} else {
mixer.update( 0 );
}
source.updateMatrixWorld();
for ( let i = 0; i < numFrames; ++ i ) {
//
const time = i * delta;
for ( let frame = 0; frame < end; ++ frame ) {
const time = frame * delta;
retarget( target, source, options );
for ( let j = 0; j < bones.length; ++ j ) {
name = options.names[ bones[ j ].name ] || bones[ j ].name;
bone = bones[ j ];
name = getBoneName( bone, options ) || bone.name;
boneTo = getBoneByName( name, source.skeleton );
if ( boneTo ) {
bone = bones[ j ];
boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone };
if ( options.hip === name ) {
@@ -246,15 +292,15 @@ function retargetClip( target, source, clip, options = {} ) {
if ( ! boneData.pos ) {
boneData.pos = {
times: new Float32Array( numFrames ),
values: new Float32Array( numFrames * 3 )
times: new Float32Array( end ),
values: new Float32Array( end * 3 )
};
}
if ( options.useFirstFramePosition ) {
if ( i === 0 ) {
if ( frame === 0 ) {
positionOffset = bone.position.clone();
@@ -264,30 +310,40 @@ function retargetClip( target, source, clip, options = {} ) {
}
boneData.pos.times[ i ] = time;
boneData.pos.times[ frame ] = time;
bone.position.toArray( boneData.pos.values, i * 3 );
bone.position.toArray( boneData.pos.values, frame * 3 );
}
if ( ! boneData.quat ) {
boneData.quat = {
times: new Float32Array( numFrames ),
values: new Float32Array( numFrames * 4 )
times: new Float32Array( end ),
values: new Float32Array( end * 4 )
};
}
boneData.quat.times[ i ] = time;
boneData.quat.times[ frame ] = time;
bone.quaternion.toArray( boneData.quat.values, i * 4 );
bone.quaternion.toArray( boneData.quat.values, frame * 4 );
}
}
mixer.update( delta );
if ( frame === end - 2 ) {
// last mixer update before final loop iteration
// make sure we do not go over or equal to clip duration
mixer.update( delta - 0.0000001 );
} else {
mixer.update( delta );
}
source.updateMatrixWorld();
@@ -325,6 +381,14 @@ function retargetClip( target, source, clip, options = {} ) {
}
/**
* Clones the given 3D object and its descendants, ensuring that any `SkinnedMesh` instances are
* correctly associated with their bones. Bones are also cloned, and must be descendants of the
* object passed to this method. Other data, like geometries and materials, are reused by reference.
*
* @param {Object3D} source - The 3D object to clone.
* @return {Object3D} The cloned 3D object.
*/
function clone( source ) {
const sourceLookup = new Map();
@@ -406,6 +470,25 @@ function parallelTraverse( a, b, callback ) {
}
/**
* Retarget options of `SkeletonUtils`.
*
* @typedef {Object} module:SkeletonUtils~RetargetOptions
* @property {boolean} [useFirstFramePosition=false] - Whether to use the position of the first frame or not.
* @property {number} [fps] - The FPS of the clip.
* @property {Object<string,string>} [names] - A dictionary for mapping target to source bone names.
* @property {function(string):string} [getBoneName] - A function for mapping bone names. Alternative to `names`.
* @property {Array<number>} [trim] - Whether to trim the clip or not. If set the array should hold two values for the start and end.
* @property {boolean} [preserveBoneMatrix=true] - Whether to preserve bone matrices or not.
* @property {boolean} [preserveBonePositions=true] - Whether to preserve bone positions or not.
* @property {boolean} [useTargetMatrix=false] - Whether to use the target matrix or not.
* @property {string} [hip='hip'] - The name of the source's hip bone.
* @property {Vector3} [hipInfluence=(1,1,1)] - The hip influence.
* @property {number} [scale=1] - The scale.
* @property {Object<string,Matrix4>} [localOffsets] - Per-bone local offset matrices, keyed by bone name.
* @property {Vector3} [hipPosition] - An additional position offset applied to the hip bone.
**/
export {
retarget,
retargetClip,
+4 -4
View File
@@ -58,7 +58,7 @@
"semver": "7.0.0",
"slugs": "0.1.3",
"source-map-explorer": "^2.0.1",
"three": "0.160.0",
"three": "0.185.1",
"url-search-params": "^1.0.2",
"wavesurfer.js": "7.12.6",
"xxhashjs": "^0.2.2",
@@ -38832,9 +38832,9 @@
}
},
"node_modules/three": {
"version": "0.160.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.160.0.tgz",
"integrity": "sha512-DLU8lc0zNIPkM7rH5/e1Ks1Z8tWCGRq6g8mPowdDJpw1CFBJMU7UoJjC6PefXW7z//SSl0b2+GCw14LB+uDhng==",
"version": "0.185.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz",
"integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==",
"license": "MIT"
},
"node_modules/throat": {
+1 -1
View File
@@ -95,7 +95,7 @@
"semver": "7.0.0",
"slugs": "0.1.3",
"source-map-explorer": "^2.0.1",
"three": "0.160.0",
"three": "0.185.1",
"url-search-params": "^1.0.2",
"wavesurfer.js": "7.12.6",
"xxhashjs": "^0.2.2",
@@ -642,12 +642,13 @@ export default class LayerRenderer {
threeScene.add(this._threeGroup);
const light = new THREE.HemisphereLight();
light.intensity = Math.PI;
light.color = new THREE.Color(1, 1, 1);
light.groundColor = new THREE.Color(0.25, 0.25, 0.25);
light.position.set(0, 0, 1);
const lightGroup = new THREE.Group();
lightGroup.rotation.order = 'ZYX';
lightGroup.rotation.x = Math.PI / 4;
lightGroup.rotation.x = -Math.PI / 4;
lightGroup.add(light);
threeScene.add(lightGroup);
-1
View File
@@ -268,7 +268,6 @@ export default class InstancesEditor extends Component<Props, State> {
const threeRenderer = new THREE.WebGLRenderer({
canvas: gameCanvas,
});
threeRenderer.useLegacyLights = true;
threeRenderer.autoClear = false;
threeRenderer.setSize(initialWidth, initialHeight);
@@ -95,7 +95,6 @@ const initRenderer = dracoDecoderFiles => {
antialias: true,
alpha: true,
});
renderer.useLegacyLights = true; // Use legacy lights as in the editor.
renderer.setSize(width, height, false);
@@ -124,6 +123,7 @@ const renderModel = async (resourceUrl, resourceData, basePath) => {
const scene = new THREE.Scene();
const light = new THREE.HemisphereLight();
light.intensity = Math.PI;
light.color = new THREE.Color(1, 1, 1);
light.groundColor = new THREE.Color(0.25, 0.25, 0.25);
light.position.set(0, 0, 1);
@@ -116,12 +116,32 @@ const getWindowTitleField = ({ i18n }: {| i18n: I18nType |}): Field => ({
visibility: 'advanced',
});
const getWorldScaleField = ({
i18n,
onRenderer3DWorldScaleFieldChanged,
}: {
i18n: I18nType,
onRenderer3DWorldScaleFieldChanged: () => void,
}): Field => ({
name: 'WorldScale',
getLabel: () => i18n._(t`3D renderer world scale`),
valueType: 'number',
getValue: (scene: gdLayout) => scene.getRenderer3DWorldScale(),
setValue: (scene: gdLayout, newValue: number) => {
scene.setRenderer3DWorldScale(newValue);
onRenderer3DWorldScaleFieldChanged();
},
visibility: 'advanced',
});
export const makeSchema = ({
i18n,
onBackgroundColorChanged,
onRenderer3DWorldScaleFieldChanged,
}: {|
i18n: I18nType,
onBackgroundColorChanged: () => void,
onRenderer3DWorldScaleFieldChanged: () => void,
|}): Schema => {
return [
{
@@ -159,5 +179,14 @@ export const makeSchema = ({
removeSpacers: true,
children: [getWindowTitleField({ i18n })],
},
{
name: 'WorldScale',
type: 'row',
preventWrap: true,
removeSpacers: true,
children: [
getWorldScaleField({ i18n, onRenderer3DWorldScaleFieldChanged }),
],
},
];
};
@@ -71,6 +71,7 @@ type Props = {|
resourceManagementProps: ResourceManagementProps,
openSceneVariables: () => void,
onBackgroundColorChanged: () => void,
onRenderer3DWorldScaleFieldChanged: () => void,
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
unsavedChanges?: ?UnsavedChanges,
i18n: I18nType,
@@ -83,6 +84,7 @@ export const CompactScenePropertiesEditor = ({
scene,
openSceneVariables,
onBackgroundColorChanged,
onRenderer3DWorldScaleFieldChanged,
projectScopedContainersAccessor,
unsavedChanges,
i18n,
@@ -150,9 +152,15 @@ export const CompactScenePropertiesEditor = ({
return makeSchema({
i18n,
onBackgroundColorChanged,
onRenderer3DWorldScaleFieldChanged,
});
},
[schemaRecomputeTrigger, i18n, onBackgroundColorChanged]
[
schemaRecomputeTrigger,
i18n,
onBackgroundColorChanged,
onRenderer3DWorldScaleFieldChanged,
]
);
return (
@@ -79,6 +79,7 @@ export type SceneEditorsDisplayProps = {|
onLayersModified: () => void,
onLayersVisibilityInEditorChanged: () => void,
onBackgroundColorChanged: () => void,
onRenderer3DWorldScaleFieldChanged: () => void,
selectedObjectGroup: gdObjectGroup | null,
onSelectObjectGroup: (objectGroup: gdObjectGroup | null) => void,
openSceneVariables: () => void,
@@ -98,6 +98,7 @@ type Props = {|
// For scenes
onBackgroundColorChanged: () => void,
onRenderer3DWorldScaleFieldChanged: () => void,
openSceneVariables: () => void,
// For object groups
@@ -213,6 +214,7 @@ export const InstanceOrObjectPropertiesEditorContainer: React.ComponentType<{
// For scenes
onBackgroundColorChanged,
onRenderer3DWorldScaleFieldChanged,
openSceneVariables,
// For object groups
@@ -356,6 +358,9 @@ export const InstanceOrObjectPropertiesEditorContainer: React.ComponentType<{
unsavedChanges={unsavedChanges}
i18n={i18n}
onBackgroundColorChanged={onBackgroundColorChanged}
onRenderer3DWorldScaleFieldChanged={
onRenderer3DWorldScaleFieldChanged
}
openSceneVariables={openSceneVariables}
/>
) : null}
@@ -360,6 +360,9 @@ const MosaicEditorsDisplay: React.ComponentType<{
props.onEventsBasedObjectChildrenEdited
}
onBackgroundColorChanged={props.onBackgroundColorChanged}
onRenderer3DWorldScaleFieldChanged={
props.onRenderer3DWorldScaleFieldChanged
}
openSceneVariables={props.openSceneVariables}
/>
)}
@@ -526,6 +526,9 @@ const SwipeableDrawerEditorsDisplay: React.ComponentType<{
onBackgroundColorChanged={
props.onBackgroundColorChanged
}
onRenderer3DWorldScaleFieldChanged={
props.onRenderer3DWorldScaleFieldChanged
}
openSceneVariables={props.openSceneVariables}
/>
)}
+24
View File
@@ -1929,6 +1929,27 @@ export default class SceneEditor extends React.Component<Props, State> {
}
};
_sendSetRenderer3DWorldScale = () => {
this.forceUpdatePropertiesEditor();
this.forceUpdateLayersList();
const { previewDebuggerServer, layout } = this.props;
if (!layout) {
return;
}
if (previewDebuggerServer) {
previewDebuggerServer
.getExistingEmbeddedGameFrameDebuggerIds()
.forEach(debuggerId => {
previewDebuggerServer.sendMessage(debuggerId, {
command: 'setRenderer3DWorldScale',
payload: {
renderer3DWorldScale: [layout.getRenderer3DWorldScale()],
},
});
});
}
};
_onLayersModified = (hasAnyEffectBeenAdded: boolean) => {
const { onEffectAdded } = this.props;
if (hasAnyEffectBeenAdded) {
@@ -3270,6 +3291,9 @@ export default class SceneEditor extends React.Component<Props, State> {
onLayerRenamed={this._onLayerRenamed}
onLayersModified={() => this._onLayersModified(false)}
onBackgroundColorChanged={this._sendSetBackgroundColor}
onRenderer3DWorldScaleFieldChanged={
this._sendSetRenderer3DWorldScale
}
onLayersVisibilityInEditorChanged={
this._onLayersVisibilityInEditorChanged
}
@@ -31,6 +31,9 @@ export const Default = (): React.Node => (
}
i18n={i18n}
onBackgroundColorChanged={action('onBackgroundColorChanged')}
onRenderer3DWorldScaleFieldChanged={action(
'onRenderer3DWorldScaleFieldChanged'
)}
openSceneVariables={action('openSceneVariables')}
/>
)}
@@ -51,6 +54,9 @@ export const Empty = (): React.Node => (
}
i18n={i18n}
onBackgroundColorChanged={action('onBackgroundColorChanged')}
onRenderer3DWorldScaleFieldChanged={action(
'onRenderer3DWorldScaleFieldChanged'
)}
openSceneVariables={action('openSceneVariables')}
/>
)}