mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-08-27 03:01:28 -04:00
deb802cfec
* For now, this is limited to instances inside custom objects. This will be made available in the future for all instances in scenes if this works well.
517 lines
16 KiB
TypeScript
517 lines
16 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.Model3DRuntimeObject}
|
|
* @category Objects > 3D Model
|
|
*/
|
|
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.
|
|
* @category Objects > 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;
|
|
_data: Model3DObjectData;
|
|
|
|
constructor(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer,
|
|
objectData: Model3DObjectData,
|
|
instanceData?: InstanceData
|
|
) {
|
|
super(instanceContainer, objectData, instanceData);
|
|
this._data = 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._crossfadeDuration = objectData.content.crossfadeDuration || 0;
|
|
|
|
this.setIsCastingShadow(objectData.content.isCastingShadow);
|
|
this.setIsReceivingShadow(objectData.content.isReceivingShadow);
|
|
this.onModelChanged(objectData);
|
|
|
|
// *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: Model3DObjectData) {
|
|
this._updateModel(objectData);
|
|
if (this._animations.length > 0) {
|
|
this._renderer.playAnimation(
|
|
this._animations[0].source,
|
|
this._animations[0].loop,
|
|
true
|
|
);
|
|
}
|
|
}
|
|
|
|
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.onModelChanged(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);
|
|
}
|
|
if (this.getInstanceContainer().getGame().isInGameEdition()) {
|
|
const oldDefaultAnimationSource =
|
|
this._animations.length > 0 ? this._animations[0].source : null;
|
|
this._animations = newObjectData.content.animations;
|
|
const newDefaultAnimationSource =
|
|
this._animations.length > 0 ? this._animations[0].source : null;
|
|
if (
|
|
newDefaultAnimationSource &&
|
|
oldDefaultAnimationSource !== newDefaultAnimationSource
|
|
) {
|
|
this._renderer.playAnimation(
|
|
newDefaultAnimationSource,
|
|
this._animations[0].loop,
|
|
true
|
|
);
|
|
}
|
|
}
|
|
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];
|
|
}
|
|
}
|
|
|
|
/** @category Objects > 3D Model */
|
|
export namespace Model3DRuntimeObject {
|
|
export enum MaterialType {
|
|
Basic,
|
|
StandardWithoutMetalness,
|
|
KeepOriginal,
|
|
}
|
|
}
|
|
gdjs.registerObject('Scene3D::Model3DObject', gdjs.Model3DRuntimeObject);
|
|
}
|