diff --git a/Extensions/3D/Cube3DRuntimeObject.ts b/Extensions/3D/Cube3DRuntimeObject.ts index b61d05e09dd9..b890d57b7279 100644 --- a/Extensions/3D/Cube3DRuntimeObject.ts +++ b/Extensions/3D/Cube3DRuntimeObject.ts @@ -24,10 +24,10 @@ namespace gdjs { rightFaceVisible: boolean; topFaceVisible: boolean; bottomFaceVisible: boolean; + tint: string; materialType: 'Basic' | 'StandardWithoutMetalness'; }; } - type FaceName = 'front' | 'back' | 'left' | 'right' | 'top' | 'bottom'; const faceNameToBitmaskIndex = { front: 0, @@ -45,6 +45,7 @@ namespace gdjs { trfb: integer; frn: [string, string, string, string, string, string]; mt: number; + tint: number; }; type Cube3DObjectNetworkSyncData = Object3DNetworkSyncData & @@ -71,7 +72,7 @@ namespace gdjs { ]; _materialType: gdjs.Cube3DRuntimeObject.MaterialType = gdjs.Cube3DRuntimeObject.MaterialType.Basic; - + _tint: number; constructor( instanceContainer: gdjs.RuntimeInstanceContainer, objectData: Cube3DObjectData @@ -117,6 +118,11 @@ namespace gdjs { objectData.content.topFaceResourceName, objectData.content.bottomFaceResourceName, ]; + + this._tint = gdjs.rgbOrHexStringToNumber( + objectData.content.tint || '255;255;255' + ); + this._materialType = this._convertMaterialType( objectData.content.materialType ); @@ -203,10 +209,15 @@ namespace gdjs { if (this._faceResourceNames[faceIndex] === resourceName) { return; } - this._faceResourceNames[faceIndex] = resourceName; this._renderer.updateFace(faceIndex); } + setTint(tint: string): void { + const tintInHex = gdjs.rgbOrHexStringToNumber(tint); + if (tintInHex === this._tint) return; + this._tint = tintInHex; + this._renderer.updateTint(); + } /** @internal */ getFaceAtIndexResourceName(faceIndex: integer): string { @@ -291,6 +302,10 @@ namespace gdjs { newObjectData.content.frontFaceResourceName ); } + if (oldObjectData.content.tint !== newObjectData.content.tint) { + this.setTint(newObjectData.content.tint); + } + if ( oldObjectData.content.backFaceResourceName !== newObjectData.content.backFaceResourceName @@ -423,6 +438,7 @@ namespace gdjs { vfb: this._visibleFacesBitmask, trfb: this._textureRepeatFacesBitmask, frn: this._faceResourceNames, + tint: this._tint, }; } @@ -476,6 +492,12 @@ namespace gdjs { } } } + if (networkSyncData.tint !== undefined) { + if (this._tint !== networkSyncData.tint) { + this._tint = networkSyncData.tint; + this._renderer.updateTint(); + } + } } /** diff --git a/Extensions/3D/Cube3DRuntimeObjectPixiRenderer.ts b/Extensions/3D/Cube3DRuntimeObjectPixiRenderer.ts index 73b9e6f1181b..2438299f9553 100644 --- a/Extensions/3D/Cube3DRuntimeObjectPixiRenderer.ts +++ b/Extensions/3D/Cube3DRuntimeObjectPixiRenderer.ts @@ -74,15 +74,13 @@ namespace gdjs { instanceContainer: gdjs.RuntimeInstanceContainer ) { const geometry = new THREE.BoxGeometry(1, 1, 1); - // TODO (3D) - feature: support color instead of texture? - const materials = [ - getFaceMaterial(runtimeObject, materialIndexToFaceIndex[0]), - getFaceMaterial(runtimeObject, materialIndexToFaceIndex[1]), - getFaceMaterial(runtimeObject, materialIndexToFaceIndex[2]), - getFaceMaterial(runtimeObject, materialIndexToFaceIndex[3]), - getFaceMaterial(runtimeObject, materialIndexToFaceIndex[4]), - getFaceMaterial(runtimeObject, materialIndexToFaceIndex[5]), - ]; + + const materials: THREE.Material[] = new Array(6) + .fill(0) + .map((_, index) => + getFaceMaterial(runtimeObject, materialIndexToFaceIndex[index]) + ); + const boxMesh = new THREE.Mesh(geometry, materials); super(runtimeObject, instanceContainer, boxMesh); @@ -92,6 +90,27 @@ namespace gdjs { this.updateSize(); this.updatePosition(); this.updateRotation(); + this.updateTint(); + } + updateTint() { + const tints: number[] = []; + + const normalizedTint = gdjs + .hexNumberToRGBArray(this._cube3DRuntimeObject._tint) + .map((component) => component / 255); + + for ( + let i = 0; + i < this._boxMesh.geometry.attributes.position.count; + i++ + ) { + tints.push(...normalizedTint); + } + + this._boxMesh.geometry.setAttribute( + 'color', + new THREE.BufferAttribute(new Float32Array(tints), 3) + ); } updateFace(faceIndex: integer) { diff --git a/Extensions/3D/JsExtension.js b/Extensions/3D/JsExtension.js index 0216ed7a4f47..ad6b8e9d4a81 100644 --- a/Extensions/3D/JsExtension.js +++ b/Extensions/3D/JsExtension.js @@ -822,7 +822,8 @@ module.exports = { propertyName === 'bottomFaceResourceName' || propertyName === 'backFaceUpThroughWhichAxisRotation' || propertyName === 'facesOrientation' || - propertyName === 'materialType' + propertyName === 'materialType' || + propertyName === 'tint' ) { objectContent[propertyName] = newValue; return true; @@ -902,6 +903,12 @@ module.exports = { .setLabel(_('Depth')) .setMeasurementUnit(gd.MeasurementUnit.getPixel()) .setGroup(_('Default size')); + objectProperties + .getOrCreate('tint') + .setValue(objectContent.tint || '255;255;255') + .setType('Color') + .setLabel(_('Tint')) + .setGroup(_('Texture')); objectProperties .getOrCreate('frontFaceResourceName') @@ -1092,6 +1099,7 @@ module.exports = { topFaceResourceRepeat: false, bottomFaceResourceRepeat: false, materialType: 'Basic', + tint: '255;255;255', }; Cube3DObject.updateInitialInstanceProperty = function ( @@ -1568,6 +1576,21 @@ module.exports = { .addParameter('imageResource', _('Image'), '', false) .setFunctionName('setFaceResourceName'); + object + .addScopedAction( + 'SetTint', + _('Tint'), + _('Change the tint of the cube.'), + _('Change the tint of _PARAM0_ to _PARAM1_'), + _('Tint'), + 'res/actions/color24.png', + 'res/actions/color.png' + ) + .addParameter('object', _('3D Cube'), 'Cube3DObject', false) + .addParameter('color', _('Tint'), '', false) + .getCodeExtraInformation() + .setFunctionName('setTint'); + extension .addExpressionAndConditionAndAction( 'number', @@ -2338,6 +2361,7 @@ module.exports = { this._facesOrientation = 'Y'; this._backFaceUpThroughWhichAxisRotation = 'X'; this._shouldUseTransparentTexture = false; + this._tint = ''; const geometry = new THREE.BoxGeometry(1, 1, 1); const materials = [ @@ -2357,8 +2381,9 @@ module.exports = { async _updateThreeObjectMaterials() { const getFaceMaterial = async (project, faceIndex) => { - if (!this._faceVisibilities[faceIndex]) + if (!this._faceVisibilities[faceIndex]) { return getTransparentMaterial(); + } return await this._pixiResourcesLoader.getThreeMaterial( project, @@ -2389,6 +2414,28 @@ module.exports = { this._updateTextureUvMapping(); } + _updateTint() { + const tints = []; + const normalizedTint = objectsRenderingService + .hexNumberToRGBArray( + objectsRenderingService.rgbOrHexToHexNumber(this._tint) + ) + .map((component) => component / 255); + + for ( + let i = 0; + i < this._threeObject.geometry.attributes.position.count; + i++ + ) { + tints.push(...normalizedTint); + } + + this._threeObject.geometry.setAttribute( + 'color', + new THREE.BufferAttribute(new Float32Array(tints), 3) + ); + } + static _getResourceNameToDisplay(objectConfiguration) { return getFirstVisibleFaceResourceName(objectConfiguration); } @@ -2421,6 +2468,7 @@ module.exports = { let materialsDirty = false; let uvMappingDirty = false; + let tintDirty = false; const shouldUseTransparentTexture = object.content.enableTextureTransparency; @@ -2428,6 +2476,11 @@ module.exports = { this._shouldUseTransparentTexture = shouldUseTransparentTexture; materialsDirty = true; } + const tint = object.content.tint || '255;255;255'; + if (this._tint !== tint) { + this._tint = tint; + tintDirty = true; + } const faceResourceNames = [ object.content.frontFaceResourceName, @@ -2520,6 +2573,7 @@ module.exports = { if (materialsDirty) this._updateThreeObjectMaterials(); if (uvMappingDirty) this._updateTextureUvMapping(); + if (tintDirty) this._updateTint(); } /** diff --git a/Extensions/JsExtensionTypes.d.ts b/Extensions/JsExtensionTypes.d.ts index 595ae275ac6d..fcfacbd9a5d7 100644 --- a/Extensions/JsExtensionTypes.d.ts +++ b/Extensions/JsExtensionTypes.d.ts @@ -189,6 +189,7 @@ declare type ObjectsRenderingService = { objectConfiguration: gd.ObjectConfiguration ) => string; rgbOrHexToHexNumber: (value: string) => number; + hexNumberToRGBArray: (value: number) => [number, number, number]; registerClearCache: (clearCache: (_: any) => void) => void; }; diff --git a/GDJS/Runtime/pixi-renderers/pixi-image-manager.ts b/GDJS/Runtime/pixi-renderers/pixi-image-manager.ts index 03cb6091f7b6..c68f40143b7f 100644 --- a/GDJS/Runtime/pixi-renderers/pixi-image-manager.ts +++ b/GDJS/Runtime/pixi-renderers/pixi-image-manager.ts @@ -242,12 +242,14 @@ namespace gdjs { map: this.getThreeTexture(resourceName), side: useTransparentTexture ? THREE.DoubleSide : THREE.FrontSide, transparent: useTransparentTexture, + vertexColors: true, }) : new THREE.MeshStandardMaterial({ map: this.getThreeTexture(resourceName), side: useTransparentTexture ? THREE.DoubleSide : THREE.FrontSide, transparent: useTransparentTexture, metalness: 0, + vertexColors: true, }); this._loadedThreeMaterials.put(cacheKey, material); return material; diff --git a/newIDE/app/src/ObjectsRendering/ObjectsRenderingService.js b/newIDE/app/src/ObjectsRendering/ObjectsRenderingService.js index 4ee215e0c0f6..6a2c74675fa2 100644 --- a/newIDE/app/src/ObjectsRendering/ObjectsRenderingService.js +++ b/newIDE/app/src/ObjectsRendering/ObjectsRenderingService.js @@ -20,7 +20,11 @@ import * as PIXI_SPINE from 'pixi-spine'; import * as THREE from 'three'; import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils'; import optionalRequire from '../Utils/OptionalRequire'; -import { rgbOrHexToHexNumber } from '../Utils/ColorTransformer'; +import { + rgbOrHexToHexNumber, + hexNumberToRGBArray, +} from '../Utils/ColorTransformer'; + const path = optionalRequire('path'); const electron = optionalRequire('electron'); const gd: libGDevelop = global.gd; @@ -277,6 +281,7 @@ const ObjectsRenderingService = { } }, rgbOrHexToHexNumber, // Expose a ColorTransformer function, useful to manage different color types for the extensions + hexNumberToRGBArray, // Expose a ColorTransformer function, useful to manage different color types for the extensions gd, // Expose gd so that it can be used by renderers PIXI, // Expose PIXI so that it can be used by renderers THREE, // Expose THREE so that it can be used by renderers diff --git a/newIDE/app/src/ObjectsRendering/PixiResourcesLoader.js b/newIDE/app/src/ObjectsRendering/PixiResourcesLoader.js index cb0789d4aad6..4fcee216748e 100644 --- a/newIDE/app/src/ObjectsRendering/PixiResourcesLoader.js +++ b/newIDE/app/src/ObjectsRendering/PixiResourcesLoader.js @@ -583,6 +583,7 @@ export default class PixiResourcesLoader { map: texture, side: useTransparentTexture ? THREE.DoubleSide : THREE.FrontSide, transparent: useTransparentTexture, + vertexColors: true, }); return material; diff --git a/newIDE/app/src/Utils/ColorTransformer.js b/newIDE/app/src/Utils/ColorTransformer.js index a46b83dd22f9..f9102bcac83e 100644 --- a/newIDE/app/src/Utils/ColorTransformer.js +++ b/newIDE/app/src/Utils/ColorTransformer.js @@ -15,6 +15,12 @@ export const rgbColorToRGBString = (rgbColor: ?RGBColor) => { return `${rgbColor.r};${rgbColor.g};${rgbColor.b}`; }; +export const hexNumberToRGBArray = ( + hexNumber: number +): [number, number, number] => { + return [(hexNumber >> 16) & 0xff, (hexNumber >> 8) & 0xff, hexNumber & 0xff]; +}; + /** * Convert a RGB color value to a Hex string. * @note No "#" or "0x" are added.