mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
492 lines
15 KiB
TypeScript
492 lines
15 KiB
TypeScript
namespace gdjs {
|
|
type Model3DAnimation = { name: string; source: string; loop: boolean };
|
|
|
|
type Model3DObjectNetworkSyncDataType = {
|
|
mt: number;
|
|
op: FloatPoint3D | null;
|
|
cp: FloatPoint3D | null;
|
|
anis: Model3DAnimation[];
|
|
ai: integer;
|
|
ass: float;
|
|
aet: float;
|
|
ap: boolean;
|
|
cfd: float;
|
|
};
|
|
|
|
type Model3DObjectNetworkSyncData = Object3DNetworkSyncData &
|
|
Model3DObjectNetworkSyncDataType;
|
|
|
|
/** Base parameters for {@link gdjs.Cube3DRuntimeObject} */
|
|
export interface Model3DObjectData extends Object3DData {
|
|
/** The base parameters of the Model3D object */
|
|
content: Object3DDataContent & {
|
|
modelResourceName: string;
|
|
rotationX: number;
|
|
rotationY: number;
|
|
rotationZ: number;
|
|
keepAspectRatio: boolean;
|
|
materialType: 'Basic' | 'StandardWithoutMetalness' | 'KeepOriginal';
|
|
originLocation:
|
|
| 'ModelOrigin'
|
|
| 'ObjectCenter'
|
|
| 'BottomCenterZ'
|
|
| 'BottomCenterY'
|
|
| 'TopLeft';
|
|
centerLocation:
|
|
| 'ModelOrigin'
|
|
| 'ObjectCenter'
|
|
| 'BottomCenterZ'
|
|
| 'BottomCenterY';
|
|
animations: Model3DAnimation[];
|
|
crossfadeDuration: float;
|
|
isCastingShadow: boolean;
|
|
isReceivingShadow: boolean;
|
|
};
|
|
}
|
|
|
|
type FloatPoint3D = [float, float, float];
|
|
|
|
const getPointForLocation = (location: string): FloatPoint3D | null => {
|
|
switch (location) {
|
|
case 'ModelOrigin':
|
|
return null;
|
|
case 'ObjectCenter':
|
|
return [0.5, 0.5, 0.5];
|
|
case 'BottomCenterZ':
|
|
return [0.5, 0.5, 0];
|
|
case 'BottomCenterY':
|
|
return [0.5, 1, 0.5];
|
|
case 'TopLeft':
|
|
return [0, 0, 0];
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A 3D object which displays a 3D model.
|
|
*/
|
|
export class Model3DRuntimeObject
|
|
extends gdjs.RuntimeObject3D
|
|
implements gdjs.Animatable
|
|
{
|
|
_renderer: gdjs.Model3DRuntimeObjectRenderer;
|
|
|
|
_modelResourceName: string;
|
|
_materialType: gdjs.Model3DRuntimeObject.MaterialType =
|
|
gdjs.Model3DRuntimeObject.MaterialType.Basic;
|
|
|
|
/**
|
|
* The local point of the model that will be at the object position.
|
|
*
|
|
* Coordinates are between 0 and 1.
|
|
*
|
|
* Its value is `null` when the point is configured to `"ModelOrigin"`
|
|
* because the model origin needs to be evaluated according to the object
|
|
* configuration.
|
|
* @see gdjs.Model3DRuntimeObject3DRenderer.getOriginPoint
|
|
*/
|
|
_originPoint: FloatPoint3D | null;
|
|
/**
|
|
* The local point of the model that is used as rotation center.
|
|
*
|
|
* Coordinates are between 0 and 1.
|
|
*
|
|
* Its value is `null` when the point is configured to `"ModelOrigin"`
|
|
* because the model origin needs to be evaluated according to the object
|
|
* configuration.
|
|
* @see gdjs.Model3DRuntimeObject3DRenderer.getCenterPoint
|
|
*/
|
|
_centerPoint: FloatPoint3D | null;
|
|
|
|
_animations: Model3DAnimation[];
|
|
_currentAnimationIndex: integer = 0;
|
|
_animationSpeedScale: float = 1;
|
|
_animationPaused: boolean = false;
|
|
_crossfadeDuration: float = 0;
|
|
_isCastingShadow: boolean = true;
|
|
_isReceivingShadow: boolean = true;
|
|
|
|
constructor(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer,
|
|
objectData: Model3DObjectData
|
|
) {
|
|
super(instanceContainer, objectData);
|
|
this._modelResourceName = objectData.content.modelResourceName;
|
|
this._animations = objectData.content.animations;
|
|
this._originPoint = getPointForLocation(
|
|
objectData.content.originLocation
|
|
);
|
|
this._centerPoint = getPointForLocation(
|
|
objectData.content.centerLocation
|
|
);
|
|
this._renderer = new gdjs.Model3DRuntimeObjectRenderer(
|
|
this,
|
|
instanceContainer
|
|
);
|
|
this._materialType = this._convertMaterialType(
|
|
objectData.content.materialType
|
|
);
|
|
|
|
this.setIsCastingShadow(objectData.content.isCastingShadow);
|
|
this.setIsReceivingShadow(objectData.content.isReceivingShadow);
|
|
this.onModelChanged(objectData);
|
|
|
|
this._crossfadeDuration = objectData.content.crossfadeDuration || 0;
|
|
|
|
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
|
|
this.onCreated();
|
|
}
|
|
|
|
/**
|
|
* To be called after the renderer loaded a Model resource:
|
|
* - After the renderer was instantiated
|
|
* - After reloading the model
|
|
*/
|
|
private onModelChanged(objectData) {
|
|
this._updateModel(objectData);
|
|
if (this._animations.length > 0) {
|
|
this._renderer.playAnimation(
|
|
this._animations[0].source,
|
|
this._animations[0].loop
|
|
);
|
|
}
|
|
}
|
|
|
|
override updateOriginalDimensionsFromObjectData(
|
|
oldObjectData: Object3DData,
|
|
newObjectData: Object3DData
|
|
): void {
|
|
// Original dimensions must not be reset by `super.updateFromObjectData`.
|
|
// `_updateModel` has a different logic to evaluate them using `keepAspectRatio`.
|
|
}
|
|
|
|
updateFromObjectData(
|
|
oldObjectData: Model3DObjectData,
|
|
newObjectData: Model3DObjectData
|
|
): boolean {
|
|
super.updateFromObjectData(oldObjectData, newObjectData);
|
|
|
|
if (
|
|
oldObjectData.content.materialType !==
|
|
newObjectData.content.materialType
|
|
) {
|
|
this._materialType = this._convertMaterialType(
|
|
newObjectData.content.materialType
|
|
);
|
|
}
|
|
if (
|
|
oldObjectData.content.modelResourceName !==
|
|
newObjectData.content.modelResourceName
|
|
) {
|
|
this._reloadModel(newObjectData);
|
|
} else if (
|
|
oldObjectData.content.width !== newObjectData.content.width ||
|
|
oldObjectData.content.height !== newObjectData.content.height ||
|
|
oldObjectData.content.depth !== newObjectData.content.depth ||
|
|
oldObjectData.content.rotationX !== newObjectData.content.rotationX ||
|
|
oldObjectData.content.rotationY !== newObjectData.content.rotationY ||
|
|
oldObjectData.content.rotationZ !== newObjectData.content.rotationZ ||
|
|
oldObjectData.content.keepAspectRatio !==
|
|
newObjectData.content.keepAspectRatio ||
|
|
oldObjectData.content.materialType !==
|
|
newObjectData.content.materialType ||
|
|
oldObjectData.content.centerLocation !==
|
|
newObjectData.content.centerLocation
|
|
) {
|
|
// The center is applied to the model by `_updateModel`.
|
|
this._centerPoint = getPointForLocation(
|
|
newObjectData.content.centerLocation
|
|
);
|
|
this._updateModel(newObjectData);
|
|
}
|
|
if (
|
|
oldObjectData.content.originLocation !==
|
|
newObjectData.content.originLocation
|
|
) {
|
|
this._originPoint = getPointForLocation(
|
|
newObjectData.content.originLocation
|
|
);
|
|
this._renderer.updatePosition();
|
|
}
|
|
if (
|
|
oldObjectData.content.isCastingShadow !==
|
|
newObjectData.content.isCastingShadow
|
|
) {
|
|
this.setIsCastingShadow(newObjectData.content.isCastingShadow);
|
|
}
|
|
if (
|
|
oldObjectData.content.isReceivingShadow !==
|
|
newObjectData.content.isReceivingShadow
|
|
) {
|
|
this.setIsReceivingShadow(newObjectData.content.isReceivingShadow);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
getNetworkSyncData(
|
|
syncOptions: GetNetworkSyncDataOptions
|
|
): Model3DObjectNetworkSyncData {
|
|
return {
|
|
...super.getNetworkSyncData(syncOptions),
|
|
mt: this._materialType,
|
|
op: this._originPoint,
|
|
cp: this._centerPoint,
|
|
anis: this._animations,
|
|
ai: this._currentAnimationIndex,
|
|
ass: this._animationSpeedScale,
|
|
aet: this.getAnimationElapsedTime(),
|
|
ap: this._animationPaused,
|
|
cfd: this._crossfadeDuration,
|
|
};
|
|
}
|
|
|
|
updateFromNetworkSyncData(
|
|
networkSyncData: Model3DObjectNetworkSyncData,
|
|
options: UpdateFromNetworkSyncDataOptions
|
|
): void {
|
|
super.updateFromNetworkSyncData(networkSyncData, options);
|
|
|
|
if (networkSyncData.mt !== undefined) {
|
|
this._materialType = networkSyncData.mt;
|
|
}
|
|
if (networkSyncData.op !== undefined) {
|
|
this._originPoint = networkSyncData.op;
|
|
}
|
|
if (networkSyncData.cp !== undefined) {
|
|
this._centerPoint = networkSyncData.cp;
|
|
}
|
|
if (networkSyncData.anis !== undefined) {
|
|
this._animations = networkSyncData.anis;
|
|
}
|
|
if (networkSyncData.ass !== undefined) {
|
|
this.setAnimationSpeedScale(networkSyncData.ass);
|
|
}
|
|
if (networkSyncData.ai !== undefined) {
|
|
this.setAnimationIndex(networkSyncData.ai);
|
|
}
|
|
if (networkSyncData.aet !== undefined) {
|
|
this.setAnimationElapsedTime(networkSyncData.aet);
|
|
}
|
|
if (networkSyncData.ap !== undefined) {
|
|
if (networkSyncData.ap !== this.isAnimationPaused()) {
|
|
networkSyncData.ap ? this.pauseAnimation() : this.resumeAnimation();
|
|
}
|
|
}
|
|
if (networkSyncData.cfd !== undefined) {
|
|
this._crossfadeDuration = networkSyncData.cfd;
|
|
}
|
|
}
|
|
|
|
_reloadModel(objectData: Model3DObjectData) {
|
|
this._modelResourceName = objectData.content.modelResourceName;
|
|
this._renderer._reloadModel(this, this._runtimeScene);
|
|
this.onModelChanged(objectData);
|
|
}
|
|
|
|
_updateModel(objectData: Model3DObjectData) {
|
|
const rotationX = objectData.content.rotationX || 0;
|
|
const rotationY = objectData.content.rotationY || 0;
|
|
const rotationZ = objectData.content.rotationZ || 0;
|
|
const width = objectData.content.width || 100;
|
|
const height = objectData.content.height || 100;
|
|
const depth = objectData.content.depth || 100;
|
|
const keepAspectRatio = objectData.content.keepAspectRatio;
|
|
this._renderer._updateModel(
|
|
rotationX,
|
|
rotationY,
|
|
rotationZ,
|
|
width,
|
|
height,
|
|
depth,
|
|
keepAspectRatio
|
|
);
|
|
}
|
|
|
|
getRenderer(): RuntimeObject3DRenderer {
|
|
return this._renderer;
|
|
}
|
|
|
|
_convertMaterialType(
|
|
materialTypeString: string
|
|
): gdjs.Model3DRuntimeObject.MaterialType {
|
|
if (materialTypeString === 'KeepOriginal') {
|
|
return gdjs.Model3DRuntimeObject.MaterialType.KeepOriginal;
|
|
} else if (materialTypeString === 'StandardWithoutMetalness') {
|
|
return gdjs.Model3DRuntimeObject.MaterialType.StandardWithoutMetalness;
|
|
} else {
|
|
return gdjs.Model3DRuntimeObject.MaterialType.Basic;
|
|
}
|
|
}
|
|
|
|
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
|
const elapsedTime = this.getElapsedTime() / 1000;
|
|
this._renderer.updateAnimation(elapsedTime);
|
|
}
|
|
|
|
/**
|
|
* Get the index of the animation being played.
|
|
* @return The index of the new animation being played
|
|
*/
|
|
getAnimationIndex(): number {
|
|
return this._currentAnimationIndex;
|
|
}
|
|
|
|
/**
|
|
* Change the animation being played.
|
|
* @param animationIndex The index of the new animation to be played
|
|
*/
|
|
setAnimationIndex(animationIndex: number): void {
|
|
animationIndex = animationIndex | 0;
|
|
if (
|
|
animationIndex < this._animations.length &&
|
|
this._currentAnimationIndex !== animationIndex &&
|
|
animationIndex >= 0
|
|
) {
|
|
const animation = this._animations[animationIndex];
|
|
this._currentAnimationIndex = animationIndex;
|
|
this._renderer.playAnimation(animation.source, animation.loop);
|
|
if (this._animationPaused) {
|
|
this._renderer.pauseAnimation();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the name of the animation being played.
|
|
* @return The name of the new animation being played
|
|
*/
|
|
getAnimationName(): string {
|
|
if (this._currentAnimationIndex >= this._animations.length) {
|
|
return '';
|
|
}
|
|
return this._animations[this._currentAnimationIndex].name;
|
|
}
|
|
|
|
/**
|
|
* Change the animation being played.
|
|
* @param newAnimationName The name of the new animation to be played
|
|
*/
|
|
setAnimationName(newAnimationName: string): void {
|
|
if (!newAnimationName) {
|
|
return;
|
|
}
|
|
const animationIndex = this._animations.findIndex(
|
|
(animation) => animation.name === newAnimationName
|
|
);
|
|
if (animationIndex >= 0) {
|
|
this.setAnimationIndex(animationIndex);
|
|
}
|
|
}
|
|
|
|
isCurrentAnimationName(name: string): boolean {
|
|
return this.getAnimationName() === name;
|
|
}
|
|
|
|
/**
|
|
* Return true if animation has ended.
|
|
* The animation had ended if:
|
|
* - it's not configured as a loop;
|
|
* - the current frame is the last frame;
|
|
* - the last frame has been displayed long enough.
|
|
*/
|
|
hasAnimationEnded(): boolean {
|
|
return this._renderer.hasAnimationEnded();
|
|
}
|
|
|
|
setIsCastingShadow(value: boolean): void {
|
|
this._isCastingShadow = value;
|
|
this._renderer._updateShadow();
|
|
}
|
|
|
|
setIsReceivingShadow(value: boolean): void {
|
|
this._isReceivingShadow = value;
|
|
this._renderer._updateShadow();
|
|
}
|
|
|
|
setCrossfadeDuration(duration: number): void {
|
|
if (this._crossfadeDuration === duration) return;
|
|
this._crossfadeDuration = duration;
|
|
}
|
|
|
|
isAnimationPaused() {
|
|
return this._animationPaused;
|
|
}
|
|
|
|
pauseAnimation() {
|
|
this._animationPaused = true;
|
|
this._renderer.pauseAnimation();
|
|
}
|
|
|
|
resumeAnimation() {
|
|
this._animationPaused = false;
|
|
this._renderer.resumeAnimation();
|
|
}
|
|
|
|
getAnimationSpeedScale() {
|
|
return this._animationSpeedScale;
|
|
}
|
|
|
|
setAnimationSpeedScale(ratio: float): void {
|
|
this._animationSpeedScale = ratio;
|
|
this._renderer.setAnimationTimeScale(ratio);
|
|
}
|
|
|
|
getAnimationElapsedTime(): float {
|
|
return this._renderer.getAnimationElapsedTime();
|
|
}
|
|
|
|
setAnimationElapsedTime(time: float): void {
|
|
this._renderer.setAnimationElapsedTime(time);
|
|
if (!this._animationPaused) {
|
|
this._renderer.resumeAnimation();
|
|
}
|
|
}
|
|
|
|
getAnimationDuration(): float {
|
|
return this._renderer.getAnimationDuration(
|
|
this._animations[this._currentAnimationIndex].source
|
|
);
|
|
}
|
|
|
|
getCenterX(): float {
|
|
const centerPoint = this._renderer.getCenterPoint();
|
|
return this.getWidth() * centerPoint[0];
|
|
}
|
|
|
|
getCenterY(): float {
|
|
const centerPoint = this._renderer.getCenterPoint();
|
|
return this.getHeight() * centerPoint[1];
|
|
}
|
|
|
|
getCenterZ(): float {
|
|
const centerPoint = this._renderer.getCenterPoint();
|
|
return this.getDepth() * centerPoint[2];
|
|
}
|
|
|
|
getDrawableX(): float {
|
|
const originPoint = this._renderer.getOriginPoint();
|
|
return this.getX() - this.getWidth() * originPoint[0];
|
|
}
|
|
|
|
getDrawableY(): float {
|
|
const originPoint = this._renderer.getOriginPoint();
|
|
return this.getY() - this.getHeight() * originPoint[1];
|
|
}
|
|
|
|
getDrawableZ(): float {
|
|
const originPoint = this._renderer.getOriginPoint();
|
|
return this.getZ() - this.getDepth() * originPoint[2];
|
|
}
|
|
}
|
|
|
|
export namespace Model3DRuntimeObject {
|
|
export enum MaterialType {
|
|
Basic,
|
|
StandardWithoutMetalness,
|
|
KeepOriginal,
|
|
}
|
|
}
|
|
gdjs.registerObject('Scene3D::Model3DObject', gdjs.Model3DRuntimeObject);
|
|
}
|