diff --git a/examples/3d-island-terrain/3d-island-terrain.json b/examples/3d-island-terrain/3d-island-terrain.json index f82fc5f49..870be9350 100644 --- a/examples/3d-island-terrain/3d-island-terrain.json +++ b/examples/3d-island-terrain/3d-island-terrain.json @@ -28880,7 +28880,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -28916,14 +28916,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -29978,8 +29986,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -29988,11 +30005,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -30001,15 +30018,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -30021,15 +30036,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -30047,15 +30063,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -30066,48 +30078,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -30139,26 +30164,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -30179,7 +30208,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -30187,12 +30239,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -30200,13 +30258,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -30265,11 +30381,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -30278,6 +30397,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -30297,8 +30417,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -30335,11 +30459,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -30571,16 +30712,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -31498,40 +31661,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -31837,14 +31981,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -32182,18 +32326,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -32425,18 +32569,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -32447,19 +32612,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -32488,17 +32656,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -32525,8 +32702,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -32620,11 +32800,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -32637,7 +32822,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -32665,14 +32851,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -32720,9 +32909,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -32741,6 +32935,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -32853,9 +33048,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -32910,12 +33105,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -33089,8 +33284,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -33387,125 +33585,375 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] } ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, - "keepRatio": true, - "layer": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -33555,8 +34003,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -33583,8 +34031,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -33611,8 +34059,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -33639,15 +34087,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -33671,8 +34121,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -33686,9 +34136,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -33696,13 +34146,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -33728,8 +34178,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -33753,9 +34203,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -33763,13 +34213,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -33793,8 +34243,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -33808,9 +34258,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -33818,13 +34268,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -33848,8 +34298,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -33863,9 +34313,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -33873,13 +34323,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -33903,8 +34353,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -33918,9 +34368,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -33928,13 +34378,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -33960,8 +34410,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -33985,9 +34435,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -33995,13 +34445,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -34025,8 +34475,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -34040,9 +34490,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -34050,13 +34500,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -34080,8 +34530,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -34095,9 +34545,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -34105,13 +34555,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -34129,12 +34579,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -34147,16 +34600,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -34174,12 +34624,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -34192,16 +34645,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -34224,8 +34674,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -34243,13 +34693,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -34272,8 +34722,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -34291,13 +34741,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -34323,8 +34773,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -34357,13 +34807,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -34385,8 +34835,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -34399,13 +34849,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -34424,12 +34874,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -34443,21 +34896,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -34479,29 +34929,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -34522,23 +34972,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -34558,8 +35008,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -34567,13 +35017,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -34591,12 +35041,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -34609,16 +35062,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -34636,12 +35086,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -34654,16 +35107,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -34681,12 +35131,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -34699,14 +35152,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -34751,6 +35304,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -34808,6 +35362,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -34819,6 +35374,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -34837,6 +35393,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -34848,6 +35405,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -34866,6 +35424,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -34877,6 +35436,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -34895,6 +35455,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -34906,6 +35467,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -34919,8 +35481,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -35000,8 +35562,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -35019,6 +35581,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -35027,7 +35620,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -35039,6 +35748,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -35126,16 +35836,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -35200,8 +35909,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -35228,8 +35937,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -35264,8 +35973,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -35292,13 +36001,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -35311,8 +36037,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -35320,8 +36046,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -35380,8 +36106,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -35412,13 +36138,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -35427,10 +36215,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -35440,6 +36229,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -35518,23 +36308,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -35592,8 +36397,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -35620,8 +36425,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -35652,8 +36457,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -35680,15 +36485,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -35711,8 +36518,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -35730,13 +36537,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -35759,8 +36566,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -35778,13 +36585,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -35800,25 +36607,117 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, "parameters": [ { - "description": "Object", - "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -35829,19 +36728,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -35854,18 +36756,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -35875,19 +36774,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -35900,20 +36802,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -35921,19 +36821,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -35946,20 +36849,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -35967,19 +36868,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -35992,33 +36896,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -36119,8 +37119,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -36128,8 +37128,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -36137,8 +37137,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -36154,17 +37154,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -36200,7 +37200,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] }, diff --git a/examples/starting-3D-platformer/starting-3D-platformer.json b/examples/starting-3D-platformer/starting-3D-platformer.json index 2a2afb982..e0cab8426 100644 --- a/examples/starting-3D-platformer/starting-3D-platformer.json +++ b/examples/starting-3D-platformer/starting-3D-platformer.json @@ -27615,7 +27615,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.2.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -27651,6 +27651,12 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], @@ -31055,7 +31061,7 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -31298,18 +31304,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -31320,19 +31347,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -31361,17 +31391,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -31398,8 +31437,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -31493,11 +31535,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -31510,7 +31557,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -31538,14 +31586,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -31593,9 +31644,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -31614,6 +31670,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -31726,9 +31783,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -31783,12 +31840,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -31962,8 +32019,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -32260,11 +32320,246 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -32278,6 +32573,9 @@ }, { "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" } ] }, @@ -34666,6 +34964,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -34759,7 +35058,23 @@ "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -35152,7 +35467,7 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "expressionType": { @@ -35223,6 +35538,100 @@ } ], "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -35268,6 +35677,12 @@ }, { "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" } ] } @@ -35278,7 +35693,7 @@ "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" @@ -35287,12 +35702,58 @@ "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", "unit": "Pixel", "label": "Height of a copy", - "description": "Copies keep the proportions of the model.", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -35430,7 +35891,7 @@ "type": "Number", "unit": "Pixel", "label": "Trunk diameter", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, @@ -35485,7 +35946,13 @@ "propertyName": "Model" }, { - "propertyName": "Size" + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" }, { "propertyName": "MinScale" @@ -35498,6 +35965,9 @@ }, { "propertyName": "RandomTint" + }, + { + "propertyName": "Size" } ] }, diff --git a/examples/starting-3d-car-racing/starting-3d-car-racing.json b/examples/starting-3d-car-racing/starting-3d-car-racing.json index 066540e30..1bbad5f90 100644 --- a/examples/starting-3d-car-racing/starting-3d-car-racing.json +++ b/examples/starting-3d-car-racing/starting-3d-car-racing.json @@ -13158,7 +13158,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.2.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -13194,6 +13194,12 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], @@ -16598,7 +16604,7 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -16841,18 +16847,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -16863,19 +16890,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -16904,17 +16934,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -16941,8 +16980,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -17036,11 +17078,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -17053,7 +17100,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -17081,14 +17129,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -17136,9 +17187,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -17157,6 +17213,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -17269,9 +17326,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -17326,12 +17383,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -17505,8 +17562,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -17803,11 +17863,246 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -17821,6 +18116,9 @@ }, { "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" } ] }, @@ -20209,6 +20507,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -20302,7 +20601,23 @@ "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -20695,7 +21010,7 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "expressionType": { @@ -20766,6 +21081,100 @@ } ], "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -20811,6 +21220,12 @@ }, { "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" } ] } @@ -20821,7 +21236,7 @@ "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" @@ -20830,12 +21245,58 @@ "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", "unit": "Pixel", "label": "Height of a copy", - "description": "Copies keep the proportions of the model.", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -20973,7 +21434,7 @@ "type": "Number", "unit": "Pixel", "label": "Trunk diameter", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, @@ -21028,7 +21489,13 @@ "propertyName": "Model" }, { - "propertyName": "Size" + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" }, { "propertyName": "MinScale" @@ -21041,6 +21508,9 @@ }, { "propertyName": "RandomTint" + }, + { + "propertyName": "Size" } ] }, diff --git a/examples/starting-3d-driving/starting-3d-driving.json b/examples/starting-3d-driving/starting-3d-driving.json index ae2301a7b..381c64f2e 100644 --- a/examples/starting-3d-driving/starting-3d-driving.json +++ b/examples/starting-3d-driving/starting-3d-driving.json @@ -12651,7 +12651,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.2.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -12687,6 +12687,12 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], @@ -16091,7 +16097,7 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -16334,18 +16340,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -16356,19 +16383,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -16397,17 +16427,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -16434,8 +16473,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -16529,11 +16571,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -16546,7 +16593,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -16574,14 +16622,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -16629,9 +16680,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -16650,6 +16706,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -16762,9 +16819,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -16819,12 +16876,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -16998,8 +17055,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -17296,11 +17356,246 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -17314,6 +17609,9 @@ }, { "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" } ] }, @@ -19702,6 +20000,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -19795,7 +20094,23 @@ "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -20188,7 +20503,7 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "expressionType": { @@ -20259,6 +20574,100 @@ } ], "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -20304,6 +20713,12 @@ }, { "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" } ] } @@ -20314,7 +20729,7 @@ "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" @@ -20323,12 +20738,58 @@ "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", "unit": "Pixel", "label": "Height of a copy", - "description": "Copies keep the proportions of the model.", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -20466,7 +20927,7 @@ "type": "Number", "unit": "Pixel", "label": "Trunk diameter", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, @@ -20521,7 +20982,13 @@ "propertyName": "Model" }, { - "propertyName": "Size" + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" }, { "propertyName": "MinScale" @@ -20534,6 +21001,9 @@ }, { "propertyName": "RandomTint" + }, + { + "propertyName": "Size" } ] }, diff --git a/examples/starting-3d-grand-theft-auto/starting-3d-grand-theft-auto.json b/examples/starting-3d-grand-theft-auto/starting-3d-grand-theft-auto.json index 27b8dd02c..88bd43ce3 100644 --- a/examples/starting-3d-grand-theft-auto/starting-3d-grand-theft-auto.json +++ b/examples/starting-3d-grand-theft-auto/starting-3d-grand-theft-auto.json @@ -28604,7 +28604,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.2.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -28640,6 +28640,12 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], @@ -32044,7 +32050,7 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -32287,18 +32293,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -32309,19 +32336,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -32350,17 +32380,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -32387,8 +32426,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -32482,11 +32524,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -32499,7 +32546,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -32527,14 +32575,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -32582,9 +32633,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -32603,6 +32659,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -32715,9 +32772,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -32772,12 +32829,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -32951,8 +33008,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -33249,11 +33309,246 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -33267,6 +33562,9 @@ }, { "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" } ] }, @@ -35655,6 +35953,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -35748,7 +36047,23 @@ "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -36141,7 +36456,7 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "expressionType": { @@ -36212,6 +36527,100 @@ } ], "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -36257,6 +36666,12 @@ }, { "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" } ] } @@ -36267,7 +36682,7 @@ "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" @@ -36276,12 +36691,58 @@ "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", "unit": "Pixel", "label": "Height of a copy", - "description": "Copies keep the proportions of the model.", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -36419,7 +36880,7 @@ "type": "Number", "unit": "Pixel", "label": "Trunk diameter", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, @@ -36474,7 +36935,13 @@ "propertyName": "Model" }, { - "propertyName": "Size" + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" }, { "propertyName": "MinScale" @@ -36487,6 +36954,9 @@ }, { "propertyName": "RandomTint" + }, + { + "propertyName": "Size" } ] }, diff --git a/examples/starting-3d-tank/starting-3d-tank.json b/examples/starting-3d-tank/starting-3d-tank.json index 4f2766be8..5586ac8dc 100644 --- a/examples/starting-3d-tank/starting-3d-tank.json +++ b/examples/starting-3d-tank/starting-3d-tank.json @@ -36633,7 +36633,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.2.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -36669,6 +36669,12 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], @@ -40073,7 +40079,7 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -40316,18 +40322,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -40338,19 +40365,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -40379,17 +40409,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -40416,8 +40455,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -40511,11 +40553,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -40528,7 +40575,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -40556,14 +40604,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -40611,9 +40662,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -40632,6 +40688,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -40744,9 +40801,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -40801,12 +40858,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -40980,8 +41037,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -41278,11 +41338,246 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -41296,6 +41591,9 @@ }, { "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" } ] }, @@ -43684,6 +43982,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -43777,7 +44076,23 @@ "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -44170,7 +44485,7 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "expressionType": { @@ -44241,6 +44556,100 @@ } ], "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -44286,6 +44695,12 @@ }, { "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" } ] } @@ -44296,7 +44711,7 @@ "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" @@ -44305,12 +44720,58 @@ "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", "unit": "Pixel", "label": "Height of a copy", - "description": "Copies keep the proportions of the model.", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -44448,7 +44909,7 @@ "type": "Number", "unit": "Pixel", "label": "Trunk diameter", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, @@ -44503,7 +44964,13 @@ "propertyName": "Model" }, { - "propertyName": "Size" + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" }, { "propertyName": "MinScale" @@ -44516,6 +44983,9 @@ }, { "propertyName": "RandomTint" + }, + { + "propertyName": "Size" } ] }, diff --git a/examples/starting-expanded-3D-platformer/starting-expanded-3D-platformer.json b/examples/starting-expanded-3D-platformer/starting-expanded-3D-platformer.json index 23f2ced35..b46eae6ec 100644 --- a/examples/starting-expanded-3D-platformer/starting-expanded-3D-platformer.json +++ b/examples/starting-expanded-3D-platformer/starting-expanded-3D-platformer.json @@ -29667,7 +29667,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -29703,14 +29703,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -30765,8 +30773,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -30775,11 +30792,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -30788,15 +30805,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -30808,15 +30823,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -30834,15 +30850,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -30853,48 +30865,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -30926,26 +30951,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -30966,7 +30995,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -30974,12 +31026,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -30987,13 +31045,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -31052,11 +31168,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -31065,6 +31184,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -31084,8 +31204,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -31122,11 +31246,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -31358,16 +31499,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -32285,40 +32448,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -32624,14 +32768,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -32969,18 +33113,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -33212,18 +33356,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -33234,19 +33399,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -33275,17 +33443,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -33312,8 +33489,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -33407,11 +33587,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -33424,7 +33609,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -33452,14 +33638,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -33507,9 +33696,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -33528,6 +33722,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -33640,9 +33835,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -33697,12 +33892,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -33876,8 +34071,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -34174,125 +34372,375 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] } ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, - "keepRatio": true, - "layer": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -34342,8 +34790,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -34370,8 +34818,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -34398,8 +34846,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -34426,15 +34874,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -34458,8 +34908,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -34473,9 +34923,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -34483,13 +34933,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -34515,8 +34965,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -34540,9 +34990,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -34550,13 +35000,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -34580,8 +35030,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -34595,9 +35045,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -34605,13 +35055,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -34635,8 +35085,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -34650,9 +35100,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -34660,13 +35110,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -34690,8 +35140,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -34705,9 +35155,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -34715,13 +35165,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -34747,8 +35197,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -34772,9 +35222,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -34782,13 +35232,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -34812,8 +35262,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -34827,9 +35277,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -34837,13 +35287,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -34867,8 +35317,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -34882,9 +35332,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -34892,13 +35342,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -34916,12 +35366,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -34934,16 +35387,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -34961,12 +35411,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -34979,16 +35432,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -35011,8 +35461,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -35030,13 +35480,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -35059,8 +35509,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -35078,13 +35528,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -35110,8 +35560,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -35144,13 +35594,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -35172,8 +35622,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -35186,13 +35636,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -35211,12 +35661,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -35230,21 +35683,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -35266,29 +35716,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -35309,23 +35759,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -35345,8 +35795,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -35354,13 +35804,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -35378,12 +35828,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -35396,16 +35849,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -35423,12 +35873,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -35441,16 +35894,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -35468,12 +35918,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -35486,14 +35939,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -35538,6 +36091,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -35595,6 +36149,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -35606,6 +36161,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -35624,6 +36180,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -35635,6 +36192,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -35653,6 +36211,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -35664,6 +36223,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -35682,6 +36242,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -35693,6 +36254,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -35706,8 +36268,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -35787,8 +36349,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -35806,6 +36368,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -35814,7 +36407,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -35826,6 +36535,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -35913,16 +36623,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -35987,8 +36696,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -36015,8 +36724,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -36051,8 +36760,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -36079,13 +36788,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -36098,8 +36824,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -36107,8 +36833,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -36167,8 +36893,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -36199,13 +36925,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -36214,10 +37002,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -36227,6 +37016,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -36305,23 +37095,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -36379,8 +37184,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -36407,8 +37212,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -36439,8 +37244,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -36467,15 +37272,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -36498,8 +37305,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -36517,13 +37324,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -36546,8 +37353,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -36565,13 +37372,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -36587,25 +37394,117 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, "parameters": [ { - "description": "Object", - "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -36616,19 +37515,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -36641,18 +37543,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -36662,19 +37561,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -36687,20 +37589,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -36708,19 +37608,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -36733,20 +37636,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -36754,19 +37655,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -36779,33 +37683,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -36906,8 +37906,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -36915,8 +37915,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -36924,8 +37924,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -36941,17 +37941,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -36987,7 +37987,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] }, diff --git a/examples/starting-expanded-3d-car-racing/starting-expanded-3d-car-racing.json b/examples/starting-expanded-3d-car-racing/starting-expanded-3d-car-racing.json index 51b454b14..38f1bf54c 100644 --- a/examples/starting-expanded-3d-car-racing/starting-expanded-3d-car-racing.json +++ b/examples/starting-expanded-3d-car-racing/starting-expanded-3d-car-racing.json @@ -15055,7 +15055,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -15091,14 +15091,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -16153,8 +16161,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -16163,11 +16180,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -16176,15 +16193,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -16196,15 +16211,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -16222,15 +16238,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -16241,48 +16253,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -16314,26 +16339,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -16354,7 +16383,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -16362,12 +16414,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -16375,13 +16433,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -16440,11 +16556,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -16453,6 +16572,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -16472,8 +16592,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -16510,11 +16634,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -16746,16 +16887,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -17673,40 +17836,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -18012,14 +18156,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -18357,18 +18501,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -18600,18 +18744,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -18622,19 +18787,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -18663,17 +18831,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -18700,8 +18877,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -18795,11 +18975,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -18812,7 +18997,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -18840,14 +19026,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -18895,9 +19084,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -18916,6 +19110,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -19028,9 +19223,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -19085,12 +19280,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -19264,8 +19459,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -19562,165 +19760,415 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 - } - ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, - "keepRatio": true, - "layer": "", - "name": "AreaPlaceholder", - "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", - "x": 0, - "y": 0, - "z": 0, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - } - ], - "editionSettings": {}, - "eventsFunctions": [ - { - "fullName": "", - "functionType": "Action", - "name": "onCreated", - "sentence": "", - "events": [ + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "Terrain3D::DefineHelperClasses" - }, - "parameters": [ - "", - "" - ] - } + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" ] }, { - "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": [ - "/** @type {gdjs.CustomRuntimeObject3D} */", - "const object = objects[0];", - "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", - " child.deleteFromScene();", - "}", - "if (object.__terrain3D) object.__terrain3D.dispose();", - "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" - ], + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false @@ -19730,8 +20178,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -19758,8 +20206,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -19786,8 +20234,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -19814,15 +20262,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -19846,8 +20296,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -19861,9 +20311,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -19871,13 +20321,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -19903,8 +20353,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -19928,9 +20378,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -19938,13 +20388,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -19968,8 +20418,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -19983,9 +20433,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -19993,13 +20443,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -20023,8 +20473,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20038,9 +20488,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -20048,13 +20498,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -20078,8 +20528,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20093,9 +20543,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -20103,13 +20553,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -20135,8 +20585,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -20160,9 +20610,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -20170,13 +20620,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -20200,8 +20650,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20215,9 +20665,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -20225,13 +20675,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -20255,8 +20705,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20270,9 +20720,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -20280,13 +20730,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -20304,12 +20754,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20322,16 +20775,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -20349,12 +20799,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20367,16 +20820,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -20399,8 +20849,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20418,13 +20868,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -20447,8 +20897,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20466,13 +20916,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -20498,8 +20948,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20532,13 +20982,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -20560,8 +21010,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20574,13 +21024,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -20599,12 +21049,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20618,21 +21071,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -20654,29 +21104,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -20697,23 +21147,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -20733,8 +21183,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -20742,13 +21192,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -20766,12 +21216,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20784,16 +21237,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -20811,12 +21261,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20829,16 +21282,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -20856,12 +21306,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20874,14 +21327,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -20926,6 +21479,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -20983,6 +21537,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -20994,6 +21549,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -21012,6 +21568,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -21023,6 +21580,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -21041,6 +21599,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -21052,6 +21611,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -21070,6 +21630,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -21081,6 +21642,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -21094,8 +21656,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -21175,8 +21737,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -21194,6 +21756,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -21202,7 +21795,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -21214,6 +21923,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -21301,16 +22011,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -21375,8 +22084,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -21403,8 +22112,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -21439,8 +22148,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -21467,13 +22176,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -21486,8 +22212,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -21495,8 +22221,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -21555,8 +22281,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -21587,13 +22313,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -21602,10 +22390,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -21615,6 +22404,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -21693,23 +22483,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -21767,8 +22572,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -21795,8 +22600,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -21827,8 +22632,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -21855,15 +22660,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -21886,8 +22693,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -21905,13 +22712,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -21934,8 +22741,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -21953,13 +22760,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -21968,32 +22775,124 @@ "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", "const object = objects[0];", - "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -22004,19 +22903,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -22029,18 +22931,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -22050,19 +22949,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -22075,20 +22977,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -22096,19 +22996,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -22121,20 +23024,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -22142,19 +23043,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -22167,33 +23071,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -22294,8 +23294,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -22303,8 +23303,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -22312,8 +23312,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -22329,17 +23329,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -22375,7 +23375,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] } diff --git a/examples/starting-expanded-3d-driving/starting-expanded-3d-driving.json b/examples/starting-expanded-3d-driving/starting-expanded-3d-driving.json index 4a994910c..a7165c2dd 100644 --- a/examples/starting-expanded-3d-driving/starting-expanded-3d-driving.json +++ b/examples/starting-expanded-3d-driving/starting-expanded-3d-driving.json @@ -14355,7 +14355,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -14391,14 +14391,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -15453,8 +15461,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -15463,11 +15480,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -15476,15 +15493,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -15496,15 +15511,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -15522,15 +15538,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -15541,48 +15553,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -15614,26 +15639,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -15654,7 +15683,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -15662,12 +15714,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -15675,13 +15733,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -15740,11 +15856,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -15753,6 +15872,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -15772,8 +15892,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -15810,11 +15934,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -16046,16 +16187,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -16973,40 +17136,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -17312,14 +17456,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -17657,18 +17801,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -17900,18 +18044,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -17922,19 +18087,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -17963,17 +18131,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -18000,8 +18177,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -18095,11 +18275,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -18112,7 +18297,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -18140,14 +18326,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -18195,9 +18384,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -18216,6 +18410,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -18328,9 +18523,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -18385,12 +18580,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -18564,8 +18759,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -18862,165 +19060,415 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 - } - ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, - "keepRatio": true, - "layer": "", - "name": "AreaPlaceholder", - "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", - "x": 0, - "y": 0, - "z": 0, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - } - ], - "editionSettings": {}, - "eventsFunctions": [ - { - "fullName": "", - "functionType": "Action", - "name": "onCreated", - "sentence": "", - "events": [ + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "Terrain3D::DefineHelperClasses" - }, - "parameters": [ - "", - "" - ] - } + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" ] }, { - "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": [ - "/** @type {gdjs.CustomRuntimeObject3D} */", - "const object = objects[0];", - "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", - " child.deleteFromScene();", - "}", - "if (object.__terrain3D) object.__terrain3D.dispose();", - "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" - ], + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false @@ -19030,8 +19478,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -19058,8 +19506,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -19086,8 +19534,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -19114,15 +19562,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -19146,8 +19596,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -19161,9 +19611,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -19171,13 +19621,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -19203,8 +19653,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -19228,9 +19678,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -19238,13 +19688,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -19268,8 +19718,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -19283,9 +19733,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -19293,13 +19743,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -19323,8 +19773,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -19338,9 +19788,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -19348,13 +19798,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -19378,8 +19828,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -19393,9 +19843,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -19403,13 +19853,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -19435,8 +19885,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -19460,9 +19910,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -19470,13 +19920,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -19500,8 +19950,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -19515,9 +19965,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -19525,13 +19975,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -19555,8 +20005,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -19570,9 +20020,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -19580,13 +20030,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -19604,12 +20054,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -19622,16 +20075,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -19649,12 +20099,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -19667,16 +20120,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -19699,8 +20149,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -19718,13 +20168,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -19747,8 +20197,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -19766,13 +20216,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -19798,8 +20248,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -19832,13 +20282,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -19860,8 +20310,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -19874,13 +20324,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -19899,12 +20349,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -19918,21 +20371,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -19954,29 +20404,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -19997,23 +20447,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -20033,8 +20483,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -20042,13 +20492,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -20066,12 +20516,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20084,16 +20537,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -20111,12 +20561,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20129,16 +20582,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -20156,12 +20606,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20174,14 +20627,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -20226,6 +20779,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -20283,6 +20837,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -20294,6 +20849,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -20312,6 +20868,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -20323,6 +20880,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -20341,6 +20899,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -20352,6 +20911,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -20370,6 +20930,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -20381,6 +20942,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -20394,8 +20956,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -20475,8 +21037,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -20494,6 +21056,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -20502,7 +21095,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -20514,6 +21223,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -20601,16 +21311,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -20675,8 +21384,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -20703,8 +21412,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -20739,8 +21448,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -20767,13 +21476,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -20786,8 +21512,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -20795,8 +21521,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -20855,8 +21581,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -20887,13 +21613,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -20902,10 +21690,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -20915,6 +21704,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -20993,23 +21783,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -21067,8 +21872,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -21095,8 +21900,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -21127,8 +21932,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -21155,15 +21960,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -21186,8 +21993,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -21205,13 +22012,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -21234,8 +22041,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -21253,13 +22060,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -21268,32 +22075,124 @@ "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", "const object = objects[0];", - "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -21304,19 +22203,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -21329,18 +22231,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -21350,19 +22249,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -21375,20 +22277,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -21396,19 +22296,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -21421,20 +22324,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -21442,19 +22343,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -21467,33 +22371,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -21594,8 +22594,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -21603,8 +22603,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -21612,8 +22612,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -21629,17 +22629,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -21675,7 +22675,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] } diff --git a/examples/starting-expanded-3d-flight-sim/starting-expanded-3d-flight-sim.json b/examples/starting-expanded-3d-flight-sim/starting-expanded-3d-flight-sim.json index 838e7e66c..52022e31d 100644 --- a/examples/starting-expanded-3d-flight-sim/starting-expanded-3d-flight-sim.json +++ b/examples/starting-expanded-3d-flight-sim/starting-expanded-3d-flight-sim.json @@ -12747,7 +12747,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -12783,14 +12783,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -13845,8 +13853,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -13855,11 +13872,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -13868,15 +13885,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -13888,15 +13903,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -13914,15 +13930,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -13933,48 +13945,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -14006,26 +14031,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -14046,7 +14075,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -14054,12 +14106,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -14067,13 +14125,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -14132,11 +14248,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -14145,6 +14264,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -14164,8 +14284,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -14202,11 +14326,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -14438,16 +14579,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -15365,40 +15528,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -15704,14 +15848,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -16049,18 +16193,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -16292,18 +16436,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -16314,19 +16479,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -16355,17 +16523,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -16392,8 +16569,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -16487,11 +16667,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -16504,7 +16689,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -16532,14 +16718,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -16587,9 +16776,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -16608,6 +16802,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -16720,9 +16915,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -16777,12 +16972,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -16956,8 +17151,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -17254,152 +17452,402 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ - { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ - { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 - } - ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, - "keepRatio": true, - "layer": "", - "name": "AreaPlaceholder", - "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", - "x": 0, - "y": 0, - "z": 0, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - } - ], - "editionSettings": {}, - "eventsFunctions": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "fullName": "", - "functionType": "Action", - "name": "onCreated", - "sentence": "", - "events": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "Terrain3D::DefineHelperClasses" - }, - "parameters": [ - "", - "" - ] - } + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } ] }, { @@ -17422,8 +17870,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -17450,8 +17898,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -17478,8 +17926,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -17506,15 +17954,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -17538,8 +17988,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -17553,9 +18003,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -17563,13 +18013,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -17595,8 +18045,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -17620,9 +18070,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -17630,13 +18080,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -17660,8 +18110,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -17675,9 +18125,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -17685,13 +18135,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -17715,8 +18165,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -17730,9 +18180,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -17740,13 +18190,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -17770,8 +18220,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -17785,9 +18235,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -17795,13 +18245,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -17827,8 +18277,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -17852,9 +18302,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -17862,13 +18312,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -17892,8 +18342,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -17907,9 +18357,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -17917,13 +18367,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -17947,8 +18397,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -17962,9 +18412,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -17972,13 +18422,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -17996,12 +18446,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -18014,16 +18467,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -18041,12 +18491,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -18058,17 +18511,14 @@ "name": "Y", "type": "expression" } - ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + ], + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -18091,8 +18541,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -18110,13 +18560,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -18139,8 +18589,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -18158,13 +18608,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -18190,8 +18640,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -18224,13 +18674,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -18252,8 +18702,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -18266,13 +18716,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -18291,12 +18741,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -18310,21 +18763,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -18346,29 +18796,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -18389,23 +18839,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -18425,8 +18875,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -18434,13 +18884,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -18458,12 +18908,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -18476,16 +18929,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -18503,12 +18953,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -18521,16 +18974,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -18548,12 +18998,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -18566,14 +19019,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -18618,6 +19171,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -18675,6 +19229,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -18686,6 +19241,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -18704,6 +19260,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -18715,6 +19272,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -18733,6 +19291,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -18744,6 +19303,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -18762,6 +19322,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -18773,6 +19334,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -18786,8 +19348,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -18867,8 +19429,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -18886,6 +19448,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -18894,7 +19487,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -18906,6 +19615,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -18993,16 +19703,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -19067,8 +19776,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -19095,8 +19804,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -19131,8 +19840,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -19159,13 +19868,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -19178,8 +19904,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -19187,8 +19913,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -19247,8 +19973,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -19279,13 +20005,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -19294,10 +20082,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -19307,6 +20096,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -19385,23 +20175,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -19459,8 +20264,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -19487,8 +20292,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -19519,8 +20324,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -19547,15 +20352,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -19578,8 +20385,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -19597,13 +20404,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -19626,8 +20433,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -19645,13 +20452,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -19667,25 +20474,117 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, "parameters": [ { - "description": "Object", - "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -19696,19 +20595,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -19721,18 +20623,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -19742,19 +20641,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -19767,20 +20669,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -19788,19 +20688,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -19813,20 +20716,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -19834,19 +20735,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -19859,33 +20763,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -19986,8 +20986,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -19995,8 +20995,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -20004,8 +21004,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -20021,17 +21021,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -20067,7 +21067,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] }, diff --git a/examples/starting-expanded-3d-grand-theft-auto/starting-expanded-3d-grand-theft-auto.json b/examples/starting-expanded-3d-grand-theft-auto/starting-expanded-3d-grand-theft-auto.json index f20c1c91b..a6e4b5da6 100644 --- a/examples/starting-expanded-3d-grand-theft-auto/starting-expanded-3d-grand-theft-auto.json +++ b/examples/starting-expanded-3d-grand-theft-auto/starting-expanded-3d-grand-theft-auto.json @@ -31884,7 +31884,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -31920,14 +31920,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -32982,8 +32990,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -32992,11 +33009,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -33005,15 +33022,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -33025,15 +33040,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -33051,15 +33067,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -33070,48 +33082,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -33143,26 +33168,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -33183,7 +33212,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -33191,12 +33243,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -33204,13 +33262,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -33269,11 +33385,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -33282,6 +33401,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -33301,8 +33421,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -33339,11 +33463,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -33575,16 +33716,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -34502,40 +34665,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -34841,14 +34985,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -35186,18 +35330,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -35429,18 +35573,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -35451,19 +35616,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -35492,17 +35660,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -35529,8 +35706,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -35624,11 +35804,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -35641,7 +35826,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -35669,14 +35855,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -35724,9 +35913,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -35745,6 +35939,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -35857,9 +36052,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -35914,12 +36109,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -36093,8 +36288,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -36391,164 +36589,414 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 - } - ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, - "keepRatio": true, - "layer": "", - "name": "AreaPlaceholder", - "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", - "x": 0, - "y": 0, - "z": 0, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - } - ], - "editionSettings": {}, - "eventsFunctions": [ - { - "fullName": "", - "functionType": "Action", - "name": "onCreated", - "sentence": "", - "events": [ + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "Terrain3D::DefineHelperClasses" - }, - "parameters": [ - "", - "" - ] - } + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" ] }, { - "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": [ - "/** @type {gdjs.CustomRuntimeObject3D} */", - "const object = objects[0];", - "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", - " child.deleteFromScene();", - "}", - "if (object.__terrain3D) object.__terrain3D.dispose();", - "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" ], "parameterObjects": "Object", "useStrict": true, @@ -36559,8 +37007,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -36587,8 +37035,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -36615,8 +37063,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -36643,15 +37091,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -36675,8 +37125,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -36690,9 +37140,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -36700,13 +37150,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -36732,8 +37182,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -36757,9 +37207,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -36767,13 +37217,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -36797,8 +37247,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -36812,9 +37262,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -36822,13 +37272,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -36852,8 +37302,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -36867,9 +37317,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -36877,13 +37327,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -36907,8 +37357,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -36922,9 +37372,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -36932,13 +37382,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -36964,8 +37414,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -36989,9 +37439,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -36999,13 +37449,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -37029,8 +37479,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -37044,9 +37494,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -37054,13 +37504,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -37084,8 +37534,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -37099,9 +37549,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -37109,13 +37559,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -37133,12 +37583,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -37151,16 +37604,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -37178,12 +37628,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -37196,16 +37649,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -37228,8 +37678,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -37247,13 +37697,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -37276,8 +37726,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -37295,13 +37745,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -37327,8 +37777,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -37361,13 +37811,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -37389,8 +37839,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -37403,13 +37853,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -37428,12 +37878,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -37447,21 +37900,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -37483,29 +37933,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -37526,23 +37976,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -37562,8 +38012,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -37571,13 +38021,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -37595,12 +38045,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -37613,16 +38066,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -37640,12 +38090,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -37658,16 +38111,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -37685,12 +38135,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -37703,14 +38156,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -37755,6 +38308,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -37812,6 +38366,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -37823,6 +38378,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -37841,6 +38397,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -37852,6 +38409,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -37870,6 +38428,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -37881,6 +38440,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -37899,6 +38459,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -37910,6 +38471,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -37923,8 +38485,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -38004,8 +38566,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -38023,6 +38585,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -38031,7 +38624,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -38043,6 +38752,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -38130,16 +38840,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -38204,8 +38913,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -38232,8 +38941,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -38268,8 +38977,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -38296,13 +39005,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -38315,8 +39041,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -38324,8 +39050,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -38384,8 +39110,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -38416,13 +39142,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -38431,10 +39219,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -38444,6 +39233,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -38522,23 +39312,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -38596,8 +39401,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -38624,8 +39429,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -38656,8 +39461,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -38684,15 +39489,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -38715,8 +39522,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -38734,13 +39541,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -38763,8 +39570,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -38782,13 +39589,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -38804,25 +39611,117 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -38833,19 +39732,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -38858,18 +39760,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -38879,19 +39778,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -38904,20 +39806,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -38925,19 +39825,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -38950,20 +39853,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -38971,19 +39872,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -38996,33 +39900,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -39123,8 +40123,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -39132,8 +40132,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -39141,8 +40141,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -39158,17 +40158,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -39204,7 +40204,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] } diff --git a/examples/starting-expanded-3d-rts-unit-selection/starting-expanded-3d-rts-unit-selection.json b/examples/starting-expanded-3d-rts-unit-selection/starting-expanded-3d-rts-unit-selection.json index 77f00a65d..2e10c665c 100644 --- a/examples/starting-expanded-3d-rts-unit-selection/starting-expanded-3d-rts-unit-selection.json +++ b/examples/starting-expanded-3d-rts-unit-selection/starting-expanded-3d-rts-unit-selection.json @@ -1941,6 +1941,7 @@ "behaviors": [], "content": { "Model": "assets\\grass_multiple_blades.glb", + "ModelAnchor": "BaseCenter", "Size": 24, "Density": 0.02, "Seed": 7, @@ -15374,7 +15375,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -15410,14 +15411,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -16472,8 +16481,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -16482,11 +16500,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -16495,15 +16513,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -16515,15 +16531,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -16541,15 +16558,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -16560,48 +16573,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -16633,26 +16659,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -16673,7 +16703,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -16681,12 +16734,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -16694,13 +16753,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -16759,11 +16876,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -16772,6 +16892,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -16791,8 +16912,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -16829,11 +16954,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -17065,16 +17207,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -17992,40 +18156,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -18331,14 +18476,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -18676,18 +18821,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -18919,18 +19064,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -18941,19 +19107,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -18982,17 +19151,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -19019,8 +19197,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -19114,11 +19295,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -19131,7 +19317,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -19159,14 +19346,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -19214,9 +19404,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -19235,6 +19430,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -19347,9 +19543,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -19404,12 +19600,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -19583,8 +19779,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -19881,125 +20080,375 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] } ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, - "keepRatio": true, - "layer": "", - "name": "AreaPlaceholder", - "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", - "x": 0, - "y": 0, - "z": 0, + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -20049,8 +20498,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -20077,8 +20526,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -20105,8 +20554,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -20133,15 +20582,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -20165,8 +20616,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20180,9 +20631,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -20190,13 +20641,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -20222,8 +20673,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -20247,9 +20698,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -20257,13 +20708,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -20287,8 +20738,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20302,9 +20753,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -20312,13 +20763,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -20342,8 +20793,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20357,9 +20808,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -20367,13 +20818,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -20397,8 +20848,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20412,9 +20863,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -20422,13 +20873,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -20454,8 +20905,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -20479,9 +20930,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -20489,13 +20940,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -20519,8 +20970,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20534,9 +20985,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -20544,13 +20995,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -20574,8 +21025,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20589,9 +21040,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -20599,13 +21050,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -20623,12 +21074,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20641,16 +21095,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -20668,12 +21119,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20686,16 +21140,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -20718,8 +21169,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20737,13 +21188,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -20766,8 +21217,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20785,13 +21236,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -20817,8 +21268,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -20851,13 +21302,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -20879,8 +21330,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20893,13 +21344,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -20918,12 +21369,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -20937,21 +21391,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -20973,29 +21424,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -21016,23 +21467,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -21052,8 +21503,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -21061,13 +21512,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -21085,12 +21536,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -21103,16 +21557,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -21130,12 +21581,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -21148,16 +21602,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -21175,12 +21626,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -21193,14 +21647,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -21245,6 +21799,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -21302,6 +21857,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -21313,6 +21869,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -21331,6 +21888,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -21342,6 +21900,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -21360,6 +21919,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -21371,6 +21931,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -21389,6 +21950,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -21400,6 +21962,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -21413,8 +21976,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -21494,8 +22057,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -21513,6 +22076,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -21521,7 +22115,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -21533,6 +22243,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -21620,16 +22331,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -21694,8 +22404,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -21722,8 +22432,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -21758,8 +22468,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -21786,13 +22496,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -21805,8 +22532,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -21814,8 +22541,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -21874,8 +22601,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -21906,13 +22633,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -21921,10 +22710,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -21934,6 +22724,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -22012,23 +22803,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -22086,8 +22892,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -22114,8 +22920,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -22146,8 +22952,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -22174,15 +22980,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -22205,8 +23013,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -22224,13 +23032,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -22253,8 +23061,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -22272,13 +23080,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -22294,25 +23102,117 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -22323,19 +23223,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -22348,18 +23251,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -22369,19 +23269,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -22394,20 +23297,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -22415,19 +23316,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -22440,20 +23344,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -22461,19 +23363,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -22486,33 +23391,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -22613,8 +23614,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -22622,8 +23623,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -22631,8 +23632,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -22648,17 +23649,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -22694,7 +23695,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] }, diff --git a/examples/starting-expanded-3d-tank/starting-expanded-3d-tank.json b/examples/starting-expanded-3d-tank/starting-expanded-3d-tank.json index 43bb77c61..660325880 100644 --- a/examples/starting-expanded-3d-tank/starting-expanded-3d-tank.json +++ b/examples/starting-expanded-3d-tank/starting-expanded-3d-tank.json @@ -38547,7 +38547,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -38583,14 +38583,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -39645,8 +39653,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -39655,11 +39672,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -39668,15 +39685,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -39688,15 +39703,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -39714,15 +39730,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -39733,48 +39745,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -39806,26 +39831,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -39846,7 +39875,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -39854,12 +39906,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -39867,13 +39925,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -39932,11 +40048,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -39945,6 +40064,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -39964,8 +40084,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -40002,11 +40126,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -40238,16 +40379,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -41165,40 +41328,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -41504,14 +41648,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -41849,18 +41993,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -42092,18 +42236,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -42114,19 +42279,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -42155,17 +42323,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -42192,8 +42369,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -42287,11 +42467,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -42304,7 +42489,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -42332,14 +42518,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -42387,9 +42576,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -42408,6 +42602,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -42520,9 +42715,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -42577,12 +42772,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -42756,8 +42951,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -43054,163 +43252,413 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 - } - ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, - "keepRatio": true, - "layer": "", - "name": "AreaPlaceholder", - "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", - "x": 0, - "y": 0, - "z": 0, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - } - ], - "editionSettings": {}, - "eventsFunctions": [ - { - "fullName": "", - "functionType": "Action", - "name": "onCreated", - "sentence": "", - "events": [ + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "Terrain3D::DefineHelperClasses" - }, - "parameters": [ - "", - "" - ] - } + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" ] }, { - "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": [ - "/** @type {gdjs.CustomRuntimeObject3D} */", - "const object = objects[0];", - "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", - " child.deleteFromScene();", - "}", - "if (object.__terrain3D) object.__terrain3D.dispose();", + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" ], "parameterObjects": "Object", @@ -43222,8 +43670,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -43250,8 +43698,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -43278,8 +43726,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -43306,15 +43754,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -43338,8 +43788,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -43353,9 +43803,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -43363,13 +43813,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -43395,8 +43845,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -43420,9 +43870,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -43430,13 +43880,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -43460,8 +43910,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -43475,9 +43925,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -43485,13 +43935,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -43515,8 +43965,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -43530,9 +43980,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -43540,13 +43990,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -43570,8 +44020,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -43585,9 +44035,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -43595,13 +44045,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -43627,8 +44077,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -43652,9 +44102,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -43662,13 +44112,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -43692,8 +44142,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -43707,9 +44157,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -43717,13 +44167,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -43747,8 +44197,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -43762,9 +44212,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -43772,13 +44222,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -43796,12 +44246,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -43814,16 +44267,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -43841,12 +44291,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -43859,16 +44312,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -43891,8 +44341,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -43910,13 +44360,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -43939,8 +44389,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -43958,13 +44408,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -43990,8 +44440,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -44024,13 +44474,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -44052,8 +44502,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -44066,13 +44516,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -44091,12 +44541,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -44110,21 +44563,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -44146,29 +44596,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -44189,23 +44639,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -44225,8 +44675,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -44234,13 +44684,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -44258,12 +44708,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -44276,16 +44729,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -44303,12 +44753,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -44321,16 +44774,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -44348,12 +44798,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -44366,14 +44819,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -44418,6 +44971,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -44475,6 +45029,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -44486,6 +45041,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -44504,6 +45060,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -44515,6 +45072,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -44533,6 +45091,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -44544,6 +45103,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -44562,6 +45122,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -44573,6 +45134,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -44586,8 +45148,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -44667,8 +45229,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -44686,6 +45248,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -44694,7 +45287,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -44706,6 +45415,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -44793,16 +45503,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -44867,8 +45576,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -44895,8 +45604,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -44931,8 +45640,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -44959,13 +45668,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -44978,8 +45704,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -44987,8 +45713,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -45047,8 +45773,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -45079,13 +45805,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -45094,10 +45882,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -45107,6 +45896,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -45185,23 +45975,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -45259,8 +46064,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -45287,8 +46092,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -45319,8 +46124,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -45347,15 +46152,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -45378,8 +46185,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -45397,13 +46204,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -45426,8 +46233,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -45445,13 +46252,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -45467,25 +46274,117 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -45496,19 +46395,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -45521,18 +46423,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -45542,19 +46441,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -45567,20 +46469,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -45588,19 +46488,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -45613,20 +46516,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -45634,19 +46535,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -45659,33 +46563,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -45786,8 +46786,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -45795,8 +46795,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -45804,8 +46804,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -45821,17 +46821,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -45867,7 +46867,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] } diff --git a/examples/starting-expanded-3d-top-down-rpg/starting-expanded-3d-top-down-rpg.json b/examples/starting-expanded-3d-top-down-rpg/starting-expanded-3d-top-down-rpg.json index 27dd78458..25fd21572 100644 --- a/examples/starting-expanded-3d-top-down-rpg/starting-expanded-3d-top-down-rpg.json +++ b/examples/starting-expanded-3d-top-down-rpg/starting-expanded-3d-top-down-rpg.json @@ -2444,6 +2444,7 @@ "behaviors": [], "content": { "Model": "assets\\grass_multiple_blades.glb", + "ModelAnchor": "BaseCenter", "Size": 40, "Density": 0.03, "Seed": 4, @@ -47143,7 +47144,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -47179,14 +47180,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -48241,8 +48250,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -48251,11 +48269,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -48264,15 +48282,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -48284,15 +48300,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -48310,15 +48327,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -48329,48 +48342,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -48402,26 +48428,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -48442,7 +48472,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -48450,12 +48503,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -48463,13 +48522,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -48528,11 +48645,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -48541,6 +48661,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -48560,8 +48681,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -48598,11 +48723,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -48834,16 +48976,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -49761,40 +49925,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -50100,14 +50245,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -50445,18 +50590,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -50688,18 +50833,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -50710,19 +50876,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -50751,17 +50920,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -50788,8 +50966,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -50883,11 +51064,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -50900,7 +51086,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -50928,14 +51115,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -50983,9 +51173,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -51004,6 +51199,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -51116,9 +51312,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -51173,12 +51369,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -51352,8 +51548,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -51650,15 +51849,265 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true + "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, "eventsBasedBehaviors": [], "eventsBasedObjects": [ { @@ -51668,10 +52117,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "Terrain", "assetStoreTag": "3d terrains", + "defaultName": "Terrain", "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", "fullName": "3D terrain", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -51759,16 +52209,15 @@ { "angle": 0, "customSize": true, - "width": 4096, - "height": 4096, "depth": 600, + "height": 4096, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -51818,8 +52267,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -51846,8 +52295,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -51874,8 +52323,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -51902,15 +52351,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -51934,8 +52385,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -51949,9 +52400,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -51959,13 +52410,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -51991,8 +52442,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -52016,9 +52467,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -52026,13 +52477,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -52056,8 +52507,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -52071,9 +52522,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -52081,13 +52532,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -52111,8 +52562,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -52126,9 +52577,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -52136,13 +52587,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -52166,8 +52617,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -52181,9 +52632,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -52191,13 +52642,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -52223,8 +52674,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -52248,9 +52699,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -52258,13 +52709,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -52288,8 +52739,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -52303,9 +52754,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -52313,13 +52764,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -52343,8 +52794,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -52358,9 +52809,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -52368,13 +52819,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -52392,12 +52843,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -52410,16 +52864,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -52437,12 +52888,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -52455,16 +52909,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -52487,8 +52938,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -52506,13 +52957,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -52535,8 +52986,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -52554,13 +53005,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -52586,8 +53037,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -52620,13 +53071,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -52648,8 +53099,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -52662,13 +53113,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -52687,12 +53138,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -52706,21 +53160,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -52742,29 +53193,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -52785,23 +53236,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -52821,8 +53272,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -52830,13 +53281,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -52854,12 +53305,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -52872,16 +53326,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -52899,12 +53350,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -52917,16 +53371,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -52944,12 +53395,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -52962,14 +53416,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -53014,6 +53568,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -53071,6 +53626,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -53082,6 +53638,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -53100,6 +53657,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -53111,6 +53669,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -53129,6 +53688,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -53140,6 +53700,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -53158,6 +53719,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -53169,6 +53731,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -53182,8 +53745,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -53263,8 +53826,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -53282,6 +53845,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -53290,7 +53884,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -53302,6 +54012,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -53389,16 +54100,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -53463,8 +54173,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -53491,8 +54201,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -53527,8 +54237,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -53555,13 +54265,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -53574,8 +54301,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -53583,8 +54310,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -53643,8 +54370,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -53675,13 +54402,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -53690,10 +54479,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -53703,6 +54493,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -53781,23 +54572,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -53855,8 +54661,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -53883,8 +54689,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -53915,8 +54721,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -53943,15 +54749,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -53974,8 +54782,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -53993,13 +54801,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -54022,8 +54830,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -54041,13 +54849,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -54063,24 +54871,24 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", "fullName": "X position of the nearest copy", "functionType": "Expression", + "group": "Copies", "name": "NearestCopyX", "sentence": "", "events": [ @@ -54099,12 +54907,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -54117,16 +54928,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", "fullName": "Y position of the nearest copy", "functionType": "Expression", + "group": "Copies", "name": "NearestCopyY", "sentence": "", "events": [ @@ -54145,12 +54953,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -54163,16 +54974,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", "fullName": "Z position of the nearest copy", "functionType": "Expression", + "group": "Copies", "name": "NearestCopyZ", "sentence": "", "events": [ @@ -54188,15 +54996,18 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -54209,16 +55020,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", "fullName": "Distance to the nearest copy", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "NearestCopyDistance", "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ @@ -54237,12 +55045,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -54255,33 +55066,223 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], "expressionType": { "type": "expression" - } + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -54382,8 +55383,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -54391,8 +55392,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -54400,8 +55401,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -54417,17 +55418,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -54463,7 +55464,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] }, diff --git a/examples/starting-expanded-first-person-farming/starting-expanded-first-person-farming.json b/examples/starting-expanded-first-person-farming/starting-expanded-first-person-farming.json index c741d7c97..8d3236898 100644 --- a/examples/starting-expanded-first-person-farming/starting-expanded-first-person-farming.json +++ b/examples/starting-expanded-first-person-farming/starting-expanded-first-person-farming.json @@ -50135,7 +50135,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -50171,14 +50171,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -51233,8 +51241,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -51243,11 +51260,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -51256,15 +51273,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -51276,15 +51291,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -51302,15 +51318,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -51321,48 +51333,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -51394,26 +51419,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -51434,7 +51463,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -51442,12 +51494,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -51455,13 +51513,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -51520,11 +51636,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -51533,6 +51652,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -51552,8 +51672,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -51590,11 +51714,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -51826,16 +51967,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -52753,40 +52916,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -53092,14 +53236,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -53437,18 +53581,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -53680,18 +53824,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -53702,19 +53867,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -53743,17 +53911,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -53780,8 +53957,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -53875,11 +54055,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -53892,7 +54077,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -53920,14 +54106,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -53975,9 +54164,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -53996,6 +54190,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -54108,9 +54303,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -54165,12 +54360,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -54344,8 +54539,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -54642,125 +54840,375 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] } ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -54810,8 +55258,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -54838,8 +55286,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -54866,8 +55314,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -54894,15 +55342,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -54926,8 +55376,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -54941,9 +55391,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -54951,13 +55401,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -54983,8 +55433,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -55008,9 +55458,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -55018,13 +55468,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -55048,8 +55498,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -55063,9 +55513,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -55073,13 +55523,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -55103,8 +55553,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -55118,9 +55568,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -55128,13 +55578,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -55158,8 +55608,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -55173,9 +55623,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -55183,13 +55633,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -55215,8 +55665,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -55240,9 +55690,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -55250,13 +55700,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -55280,8 +55730,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -55295,9 +55745,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -55305,13 +55755,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -55335,8 +55785,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -55350,9 +55800,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -55360,13 +55810,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -55384,12 +55834,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -55402,16 +55855,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -55429,12 +55879,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -55447,16 +55900,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -55479,8 +55929,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -55498,13 +55948,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -55527,8 +55977,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -55546,13 +55996,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -55578,8 +56028,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -55612,13 +56062,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -55640,8 +56090,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -55654,13 +56104,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -55679,12 +56129,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -55698,21 +56151,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -55734,29 +56184,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -55777,23 +56227,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -55813,8 +56263,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -55822,13 +56272,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -55846,12 +56296,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -55864,16 +56317,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -55891,12 +56341,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -55909,16 +56362,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -55936,12 +56386,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -55954,14 +56407,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -56006,6 +56559,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -56063,6 +56617,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -56074,6 +56629,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -56092,6 +56648,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -56103,6 +56660,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -56121,6 +56679,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -56132,6 +56691,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -56150,6 +56710,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -56161,6 +56722,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -56174,8 +56736,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -56255,8 +56817,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -56274,6 +56836,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -56282,7 +56875,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -56294,6 +57003,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -56381,16 +57091,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -56455,8 +57164,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -56483,8 +57192,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -56519,8 +57228,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -56547,13 +57256,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -56566,8 +57292,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -56575,8 +57301,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -56635,8 +57361,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -56667,13 +57393,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -56682,10 +57470,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -56695,6 +57484,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -56773,23 +57563,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -56847,8 +57652,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -56875,8 +57680,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -56907,8 +57712,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -56935,15 +57740,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -56966,8 +57773,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -56985,13 +57792,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -57014,8 +57821,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -57033,13 +57840,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -57055,25 +57862,117 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, "parameters": [ { - "description": "Object", - "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -57084,19 +57983,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -57109,18 +58011,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -57130,19 +58029,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -57155,20 +58057,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -57176,19 +58076,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -57201,20 +58104,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -57222,19 +58123,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -57247,33 +58151,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -57374,8 +58374,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -57383,8 +58383,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -57392,8 +58392,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -57409,17 +58409,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -57455,7 +58455,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] }, diff --git a/examples/starting-expanded-first-person-horror/starting-expanded-first-person-horror.json b/examples/starting-expanded-first-person-horror/starting-expanded-first-person-horror.json index 7fb72d2ea..f832e279b 100644 --- a/examples/starting-expanded-first-person-horror/starting-expanded-first-person-horror.json +++ b/examples/starting-expanded-first-person-horror/starting-expanded-first-person-horror.json @@ -30763,7 +30763,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -30799,14 +30799,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -31861,8 +31869,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -31871,11 +31888,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -31884,15 +31901,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -31904,15 +31919,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -31930,15 +31946,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -31949,48 +31961,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -32022,26 +32047,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -32062,7 +32091,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -32070,12 +32122,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -32083,13 +32141,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -32148,11 +32264,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -32161,6 +32280,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -32180,8 +32300,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -32218,11 +32342,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -32454,16 +32595,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -33381,40 +33544,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -33720,14 +33864,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -34065,18 +34209,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -34308,18 +34452,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -34330,19 +34495,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -34371,17 +34539,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -34408,8 +34585,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -34503,11 +34683,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -34520,7 +34705,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -34548,14 +34734,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -34603,9 +34792,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -34624,6 +34818,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -34736,9 +34931,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -34793,12 +34988,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -34972,8 +35167,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -35270,164 +35468,414 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 - } - ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, - "keepRatio": true, - "layer": "", - "name": "AreaPlaceholder", - "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", - "x": 0, - "y": 0, - "z": 0, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - } - ], - "editionSettings": {}, - "eventsFunctions": [ - { - "fullName": "", - "functionType": "Action", - "name": "onCreated", - "sentence": "", - "events": [ + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "Terrain3D::DefineHelperClasses" - }, - "parameters": [ - "", - "" - ] - } + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" ] }, { - "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": [ - "/** @type {gdjs.CustomRuntimeObject3D} */", - "const object = objects[0];", - "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", - " child.deleteFromScene();", - "}", - "if (object.__terrain3D) object.__terrain3D.dispose();", - "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" ], "parameterObjects": "Object", "useStrict": true, @@ -35438,8 +35886,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -35466,8 +35914,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -35494,8 +35942,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -35522,15 +35970,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -35554,8 +36004,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -35569,9 +36019,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -35579,13 +36029,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -35611,8 +36061,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -35636,9 +36086,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -35646,13 +36096,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -35676,8 +36126,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -35691,9 +36141,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -35701,13 +36151,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -35731,8 +36181,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -35746,9 +36196,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -35756,13 +36206,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -35786,8 +36236,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -35801,9 +36251,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -35811,13 +36261,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -35843,8 +36293,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -35868,9 +36318,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -35878,13 +36328,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -35908,8 +36358,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -35923,9 +36373,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -35933,13 +36383,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -35963,8 +36413,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -35978,9 +36428,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -35988,13 +36438,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -36012,12 +36462,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -36030,16 +36483,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -36057,12 +36507,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -36075,16 +36528,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -36107,8 +36557,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -36126,13 +36576,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -36155,8 +36605,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -36174,13 +36624,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -36206,8 +36656,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -36240,13 +36690,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -36268,8 +36718,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -36282,13 +36732,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -36307,12 +36757,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -36326,21 +36779,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -36362,29 +36812,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -36405,23 +36855,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -36441,8 +36891,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -36450,13 +36900,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -36474,12 +36924,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -36492,16 +36945,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -36519,12 +36969,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -36537,16 +36990,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -36564,12 +37014,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -36582,14 +37035,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -36634,6 +37187,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -36691,6 +37245,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -36702,6 +37257,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -36720,6 +37276,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -36731,6 +37288,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -36749,6 +37307,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -36760,6 +37319,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -36778,6 +37338,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -36789,6 +37350,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -36802,8 +37364,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -36883,8 +37445,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -36902,6 +37464,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -36910,7 +37503,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -36922,6 +37631,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -37009,16 +37719,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -37083,8 +37792,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -37111,8 +37820,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -37147,8 +37856,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -37175,13 +37884,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -37194,8 +37920,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -37203,8 +37929,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -37263,8 +37989,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -37295,13 +38021,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -37310,10 +38098,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -37323,6 +38112,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -37401,23 +38191,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -37475,8 +38280,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -37503,8 +38308,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -37535,8 +38340,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -37563,15 +38368,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -37594,8 +38401,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -37613,13 +38420,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -37642,8 +38449,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -37661,13 +38468,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -37676,32 +38483,124 @@ "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", "const object = objects[0];", - "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -37712,19 +38611,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -37737,18 +38639,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -37758,19 +38657,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -37783,20 +38685,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -37804,19 +38704,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -37829,20 +38732,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -37850,19 +38751,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -37875,33 +38779,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -38002,8 +39002,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -38011,8 +39011,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -38020,8 +39020,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -38037,17 +39037,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -38083,7 +39083,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] } diff --git a/examples/starting-expanded-first-person-shooter-enemies/starting-expanded-first-person-shooter-enemies.json b/examples/starting-expanded-first-person-shooter-enemies/starting-expanded-first-person-shooter-enemies.json index 313053e1b..12d41cb6b 100644 --- a/examples/starting-expanded-first-person-shooter-enemies/starting-expanded-first-person-shooter-enemies.json +++ b/examples/starting-expanded-first-person-shooter-enemies/starting-expanded-first-person-shooter-enemies.json @@ -42455,7 +42455,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -42491,14 +42491,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -43553,8 +43561,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -43563,11 +43580,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -43576,15 +43593,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -43596,15 +43611,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -43622,15 +43638,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -43641,48 +43653,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -43714,26 +43739,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -43754,7 +43783,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -43762,12 +43814,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -43775,13 +43833,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -43840,11 +43956,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -43853,6 +43972,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -43872,8 +43992,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -43910,11 +44034,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -44146,16 +44287,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -45073,40 +45236,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -45412,14 +45556,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -45757,18 +45901,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -46000,18 +46144,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -46022,19 +46187,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -46063,17 +46231,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -46100,8 +46277,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -46195,11 +46375,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -46212,7 +46397,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -46240,14 +46426,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -46295,9 +46484,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -46316,6 +46510,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -46428,9 +46623,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -46485,12 +46680,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -46664,8 +46859,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -46962,163 +47160,413 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 - } - ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, - "keepRatio": true, - "layer": "", - "name": "AreaPlaceholder", - "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", - "x": 0, - "y": 0, - "z": 0, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - } - ], - "editionSettings": {}, - "eventsFunctions": [ - { - "fullName": "", - "functionType": "Action", - "name": "onCreated", - "sentence": "", - "events": [ + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "Terrain3D::DefineHelperClasses" - }, - "parameters": [ - "", - "" - ] - } + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" ] }, { - "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": [ - "/** @type {gdjs.CustomRuntimeObject3D} */", - "const object = objects[0];", - "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", - " child.deleteFromScene();", - "}", - "if (object.__terrain3D) object.__terrain3D.dispose();", + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" ], "parameterObjects": "Object", @@ -47130,8 +47578,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -47158,8 +47606,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -47186,8 +47634,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -47214,15 +47662,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -47246,8 +47696,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -47261,9 +47711,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -47271,13 +47721,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -47303,8 +47753,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -47328,9 +47778,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -47338,13 +47788,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -47368,8 +47818,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -47383,9 +47833,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -47393,13 +47843,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -47423,8 +47873,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -47438,9 +47888,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -47448,13 +47898,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -47478,8 +47928,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -47493,9 +47943,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -47503,13 +47953,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -47535,8 +47985,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -47560,9 +48010,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -47570,13 +48020,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -47600,8 +48050,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -47615,9 +48065,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -47625,13 +48075,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -47655,8 +48105,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -47670,9 +48120,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -47680,13 +48130,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -47704,12 +48154,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -47722,16 +48175,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -47749,12 +48199,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -47767,16 +48220,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -47799,8 +48249,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -47818,13 +48268,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -47847,8 +48297,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -47866,13 +48316,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -47898,8 +48348,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -47932,13 +48382,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -47960,8 +48410,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -47974,13 +48424,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -47999,12 +48449,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -48018,21 +48471,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -48054,29 +48504,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -48097,23 +48547,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -48133,8 +48583,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -48142,13 +48592,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -48166,12 +48616,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -48184,16 +48637,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -48211,12 +48661,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -48229,16 +48682,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -48256,12 +48706,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -48274,14 +48727,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -48326,6 +48879,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -48383,6 +48937,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -48394,6 +48949,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -48412,6 +48968,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -48423,6 +48980,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -48441,6 +48999,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -48452,6 +49011,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -48470,6 +49030,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -48481,6 +49042,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -48494,8 +49056,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -48575,8 +49137,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -48594,6 +49156,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -48602,7 +49195,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -48614,6 +49323,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -48701,16 +49411,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -48775,8 +49484,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -48803,8 +49512,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -48839,8 +49548,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -48867,13 +49576,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -48886,8 +49612,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -48895,8 +49621,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -48955,8 +49681,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -48987,13 +49713,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -49002,10 +49790,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -49015,6 +49804,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -49093,23 +49883,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -49167,8 +49972,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -49195,8 +50000,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -49227,8 +50032,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -49255,15 +50060,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -49286,8 +50093,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -49305,13 +50112,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -49334,8 +50141,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -49353,13 +50160,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -49368,32 +50175,124 @@ "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", "const object = objects[0];", - "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -49404,19 +50303,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -49429,18 +50331,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -49450,19 +50349,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -49475,20 +50377,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -49496,19 +50396,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -49521,20 +50424,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -49542,19 +50443,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -49567,33 +50471,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -49694,8 +50694,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -49703,8 +50703,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -49712,8 +50712,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -49729,17 +50729,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -49775,7 +50775,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] } diff --git a/examples/starting-expanded-first-person-shooter-horror/starting-expanded-first-person-shooter-horror.json b/examples/starting-expanded-first-person-shooter-horror/starting-expanded-first-person-shooter-horror.json index 7a3d7979e..6a1db37a2 100644 --- a/examples/starting-expanded-first-person-shooter-horror/starting-expanded-first-person-shooter-horror.json +++ b/examples/starting-expanded-first-person-shooter-horror/starting-expanded-first-person-shooter-horror.json @@ -48016,7 +48016,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -48052,14 +48052,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -49114,8 +49122,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -49124,11 +49141,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -49137,15 +49154,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -49157,15 +49172,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -49183,15 +49199,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -49202,48 +49214,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -49275,26 +49300,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -49315,7 +49344,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -49323,12 +49375,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -49336,13 +49394,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -49401,11 +49517,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -49414,6 +49533,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -49433,8 +49553,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -49471,11 +49595,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -49707,16 +49848,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -50634,40 +50797,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -50973,14 +51117,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -51318,18 +51462,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -51561,18 +51705,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -51583,19 +51748,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -51624,17 +51792,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -51661,8 +51838,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -51756,11 +51936,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -51773,7 +51958,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -51801,14 +51987,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -51856,9 +52045,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -51877,6 +52071,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -51989,9 +52184,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -52046,12 +52241,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -52225,8 +52420,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -52523,163 +52721,413 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 - } - ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, - "keepRatio": true, - "layer": "", - "name": "AreaPlaceholder", - "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", - "x": 0, - "y": 0, - "z": 0, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - } - ], - "editionSettings": {}, - "eventsFunctions": [ - { - "fullName": "", - "functionType": "Action", - "name": "onCreated", - "sentence": "", - "events": [ + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "Terrain3D::DefineHelperClasses" - }, - "parameters": [ - "", - "" - ] - } + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" ] }, { - "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": [ - "/** @type {gdjs.CustomRuntimeObject3D} */", - "const object = objects[0];", - "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", - " child.deleteFromScene();", - "}", - "if (object.__terrain3D) object.__terrain3D.dispose();", + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" ], "parameterObjects": "Object", @@ -52691,8 +53139,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -52719,8 +53167,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -52747,8 +53195,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -52775,15 +53223,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -52807,8 +53257,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -52822,9 +53272,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -52832,13 +53282,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -52864,8 +53314,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -52889,9 +53339,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -52899,13 +53349,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -52929,8 +53379,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -52944,9 +53394,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -52954,13 +53404,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -52984,8 +53434,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -52999,9 +53449,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -53009,13 +53459,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -53039,8 +53489,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -53054,9 +53504,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -53064,13 +53514,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -53096,8 +53546,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -53121,9 +53571,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -53131,13 +53581,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -53161,8 +53611,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -53176,9 +53626,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -53186,13 +53636,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -53216,8 +53666,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -53231,9 +53681,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -53241,13 +53691,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -53265,12 +53715,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -53283,16 +53736,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -53310,12 +53760,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -53328,16 +53781,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -53360,8 +53810,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -53379,13 +53829,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -53408,8 +53858,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -53427,13 +53877,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -53459,8 +53909,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -53493,13 +53943,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -53521,8 +53971,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -53535,13 +53985,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -53560,12 +54010,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -53579,21 +54032,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -53615,29 +54065,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -53658,23 +54108,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -53694,8 +54144,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -53703,13 +54153,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -53727,12 +54177,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -53745,16 +54198,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -53772,12 +54222,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -53790,16 +54243,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -53817,12 +54267,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -53835,14 +54288,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -53887,6 +54440,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -53944,6 +54498,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -53955,6 +54510,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -53973,6 +54529,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -53984,6 +54541,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -54002,6 +54560,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -54013,6 +54572,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -54031,6 +54591,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -54042,6 +54603,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -54055,8 +54617,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -54136,8 +54698,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -54155,6 +54717,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -54163,7 +54756,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -54175,6 +54884,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -54262,16 +54972,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -54336,8 +55045,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -54364,8 +55073,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -54400,8 +55109,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -54428,13 +55137,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -54447,8 +55173,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -54456,8 +55182,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -54516,8 +55242,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -54548,13 +55274,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -54563,10 +55351,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -54576,6 +55365,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -54654,23 +55444,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -54728,8 +55533,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -54756,8 +55561,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -54788,8 +55593,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -54816,15 +55621,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -54847,8 +55654,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -54866,13 +55673,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -54895,8 +55702,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -54914,13 +55721,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -54929,32 +55736,124 @@ "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", "const object = objects[0];", - "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -54965,19 +55864,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -54990,18 +55892,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -55011,19 +55910,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -55036,20 +55938,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -55057,19 +55957,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -55082,20 +55985,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -55103,19 +56004,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -55128,33 +56032,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -55255,8 +56255,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -55264,8 +56264,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -55273,8 +56273,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -55290,17 +56290,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -55336,7 +56336,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] } diff --git a/examples/starting-expanded-first-person-survival-crafting/starting-expanded-first-person-survival-crafting.json b/examples/starting-expanded-first-person-survival-crafting/starting-expanded-first-person-survival-crafting.json index 00a46487a..d1ea8b7e3 100644 --- a/examples/starting-expanded-first-person-survival-crafting/starting-expanded-first-person-survival-crafting.json +++ b/examples/starting-expanded-first-person-survival-crafting/starting-expanded-first-person-survival-crafting.json @@ -37709,7 +37709,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.1.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -37745,14 +37745,22 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], "eventsFunctions": [ { + "description": "Define helper classes JavaScript code.", "fullName": "Define helper classes", "functionType": "Action", "name": "DefineHelperClasses", + "private": true, "sentence": "Define helper classes JavaScript code", "events": [ { @@ -38807,8 +38815,17 @@ "vTerrainLocalXY = position.xy;", "vTerrainLocalZ = position.z;", "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", - "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "// The world normal, from the camera space normal (computed by Three.js with a", + "// matrix inverted on the CPU), instead of inverting a matrix for each vertex.", + "vTerrainUp = inverseTransformDirection(transformedNormal, viewMatrix).z;", "`;", + "/** @param {(layer: number) => string} getLayerCode */", + "const forEachLayerCode = (getLayerCode) => [0, 1, 2, 3].map(getLayerCode).join('\\n');", + "", + "// Noise is read from a texture of random values, repeated every", + "// NOISE_MAP_SIZE units of noise: 1 texture read instead of 16 hashes.", + "const NOISE_MAP_SIZE = 256;", + "", "const fragmentShaderDeclarations = `", "uniform sampler2D terrainSplatMap;", "uniform vec2 terrainSplatUvScaleAndOffset;", @@ -38817,11 +38834,11 @@ "uniform sampler2D terrainLayerMap1;", "uniform sampler2D terrainLayerMap2;", "uniform sampler2D terrainLayerMap3;", + "uniform vec3 terrainLayerAverageColors[4];", "uniform sampler2D terrainLayerNormalMap0;", "uniform sampler2D terrainLayerNormalMap1;", "uniform sampler2D terrainLayerNormalMap2;", "uniform sampler2D terrainLayerNormalMap3;", - "uniform float terrainHasNormalMaps;", "uniform float terrainNormalMapStrength;", "uniform float terrainTextureSize;", "uniform float terrainHeightBlending;", @@ -38830,15 +38847,13 @@ "uniform vec2 terrainCliffCosines;", "uniform vec4 terrainBrush;", "uniform sampler2D terrainColorMap;", + "uniform sampler2D terrainNoiseMap;", "uniform vec3 terrainWetShore;", "varying vec2 vTerrainLocalXY;", "varying float vTerrainLocalZ;", "varying vec2 vTerrainWorldXY;", "varying float vTerrainUp;", "", - "float terrainHash(vec2 p) {", - " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", - "}", "// The rotation of the copies of textures used to make repetition less visible.", "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", @@ -38850,15 +38865,16 @@ " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", " return mix(bump, copyBump, patches);", "}", - "float terrainNoise(vec2 p) {", + "/**", + " * 4 independent value noises (one by channel), from 0 to 1, smoothly varying", + " * between random values at integer positions.", + " */", + "vec4 terrainNoise(vec2 p) {", " vec2 i = floor(p);", - " vec2 f = fract(p);", - " vec2 u = f * f * (3.0 - 2.0 * f);", - " return mix(", - " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", - " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", - " u.y", - " );", + " vec2 f = p - i;", + " // The read position is moved so that the linear interpolation of the", + " // texture between 2 random values becomes a smooth one.", + " return texture2D(terrainNoiseMap, (i + f * f * (3.0 - 2.0 * f) + 0.5) / ${NOISE_MAP_SIZE}.0);", "}", "`;", "// Holes are samples without any painted layer: the interpolated sum of the", @@ -38876,15 +38892,11 @@ "varying vec2 vTerrainLocalXY;", "`;", "", + "// Layers without texture, normal map, height blending or texture variation", + "// are compiled out (see TerrainRenderer.updateMaterial).", "const fragmentShaderCode = holeShaderCode + `", "// Borders between layers are made irregular and sharper, to look painted.", - "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", - "vec4 terrainJitter = vec4(", - " terrainNoise(terrainNoisePosition),", - " terrainNoise(terrainNoisePosition + 19.1),", - " terrainNoise(terrainNoisePosition + 37.7),", - " terrainNoise(terrainNoisePosition + 53.3)", - ");", + "vec4 terrainJitter = terrainNoise(vTerrainWorldXY / 24.0);", "// Layer weights are in a texture, so that painted details stay the same", "// whatever the level of detail of the ground.", "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", @@ -38895,48 +38907,61 @@ "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", "", "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", - "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", - "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", - "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", - "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "vec3 terrainTexture${layer} = texture2D(terrainLayerMap${layer}, terrainUv).rgb;", + "#else", + "vec3 terrainTexture${layer} = vec3(1.0);", + "#endif`", + ")}", "", "// Less visible repetition: in irregular patches, textures are replaced by a", "// rotated and shifted copy of themselves, and colors slowly vary.", "float terrainPatches = 0.0;", "vec2 terrainUv2 = terrainUv;", "float terrainColorVariation = 1.0;", - "if (terrainTextureVariation > 0.0) {", - " // Up to half of the ground is covered by the copies, each patch showing", - " // only one of them (mixing them would make them blurry).", - " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", - " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", - " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", - " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", - " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", - " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", - " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", - " terrainColorVariation += terrainTextureVariation * 0.3 *", - " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", - "}", + "#ifdef TERRAIN_TEXTURE_VARIATION", + "// Up to half of the ground is covered by the copies, each patch showing", + "// only one of them (mixing them would make them blurry).", + "float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + "terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3).x);", + "terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MAP_${layer}", + "terrainTexture${layer} = mix(terrainTexture${layer}, texture2D(terrainLayerMap${layer}, terrainUv2).rgb, terrainPatches);", + "#endif`", + ")}", + "terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11).y + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1).z - 0.75);", + "#endif", "", "// Height-based blending: the brightness of a texture, compared to its", "// average, is used as its height. Where layers meet, the high parts (stones,", "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", - "if (terrainHeightBlending > 0.0) {", - " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", - " vec4 terrainHeights = vec4(", - " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", - " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", - " );", - " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", - " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", - " // Only painted layers (and the cliff layer) can show.", - " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", - " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", - " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", - "}", + "#ifdef TERRAIN_HEIGHT_BLENDING", + "// Averages are computed once on the CPU, or read from the smallest mipmap", + "// when the image can't be read by the CPU.", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_MIPMAP_AVERAGE_${layer}", + "vec3 terrainAverage${layer} = textureLod(terrainLayerMap${layer}, vec2(0.5), 16.0).rgb;", + "#else", + "vec3 terrainAverage${layer} = terrainLayerAverageColors[${layer}];", + "#endif`", + ")}", + "const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + "vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - terrainAverage0, terrainLuminance),", + " dot(terrainTexture1 - terrainAverage1, terrainLuminance),", + " dot(terrainTexture2 - terrainAverage2, terrainLuminance),", + " dot(terrainTexture3 - terrainAverage3, terrainLuminance)", + ");", + "vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + "float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + "// Only painted layers (and the cliff layer) can show.", + "vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + "terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + "terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "#endif", "", "vec3 terrainColor = terrainColorVariation * (", " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", @@ -38968,26 +38993,30 @@ "`;", "", "const fragmentShaderNormalCode = `", - "if (terrainHasNormalMaps > 0.5) {", - " vec3 terrainMapNormal =", - " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", - " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", - " terrainMapNormal.xy *= terrainNormalMapStrength;", - " // The X and Y of textures follow the X and Y of the 3D world, projected on", - " // the ground (the camera space of the normal is used).", - " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "#ifdef TERRAIN_NORMAL_MAPS", + "// Layers without normal map are flat.", + "vec3 terrainMapNormal = vec3(0.0);", + "${forEachLayerCode(", + " (layer) => `#ifdef TERRAIN_LAYER_NORMAL_MAP_${layer}", + "terrainMapNormal += terrainWeights[${layer}] * terrainSampleNormal(terrainLayerNormalMap${layer}, terrainUv, terrainUv2, terrainPatches);", + "#else", + "terrainMapNormal.z += terrainWeights[${layer}];", + "#endif`", + ")}", + "terrainMapNormal.xy *= terrainNormalMapStrength;", + "// The X and Y of textures follow the X and Y of the 3D world, projected on", + "// the ground (the camera space of the normal is used).", + "vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + "terrainTangent -= normal * dot(normal, terrainTangent);", + "// A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + "if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", " terrainTangent -= normal * dot(normal, terrainTangent);", - " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", - " if (dot(terrainTangent, terrainTangent) < 0.000001) {", - " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", - " terrainTangent -= normal * dot(normal, terrainTangent);", - " }", - " terrainTangent = normalize(terrainTangent);", - " vec3 terrainBitangent = cross(normal, terrainTangent);", - " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", "}", + "terrainTangent = normalize(terrainTangent);", + "vec3 terrainBitangent = cross(normal, terrainTangent);", + "normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "#endif", "`;", "", "let whiteTexture = null;", @@ -39008,7 +39037,30 @@ " return flatNormalTexture;", "};", "", + "let noiseTexture = null;", + "/** Random values, in 4 independent channels, read by `terrainNoise` in the shader. */", + "const getNoiseTexture = () => {", + " if (!noiseTexture) {", + " const random = createRandom(1);", + " const bytes = new Uint8Array(NOISE_MAP_SIZE * NOISE_MAP_SIZE * 4);", + " for (let index = 0; index < bytes.length; index++) {", + " bytes[index] = Math.floor(random() * 256);", + " }", + " noiseTexture = new THREE.DataTexture(bytes, NOISE_MAP_SIZE, NOISE_MAP_SIZE);", + " noiseTexture.wrapS = THREE.RepeatWrapping;", + " noiseTexture.wrapT = THREE.RepeatWrapping;", + " // No mipmaps: the shader moves the read position between samples.", + " noiseTexture.magFilter = THREE.LinearFilter;", + " noiseTexture.minFilter = THREE.LinearFilter;", + " noiseTexture.needsUpdate = true;", + " }", + " return noiseTexture;", + "};", + "", + "const DEFAULT_ANISOTROPY = 4;", + "/** @type {WeakMap>} */", "const layerTextures = new WeakMap();", + "/** @type {WeakMap>} */", "const layerNormalMaps = new WeakMap();", "/**", " * Textures of the image manager have no mipmaps: they would shimmer when", @@ -39016,12 +39068,18 @@ " * @param {gdjs.RuntimeGame} game", " * @param {string} resourceName", " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " * @param {number} anisotropy", " */", - "const getLayerTexture = (game, resourceName, isNormalMap) => {", + "const getLayerTexture = (game, resourceName, isNormalMap, anisotropy) => {", " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", - " const textures = isNormalMap ? layerNormalMaps : layerTextures;", - " let texture = textures.get(imageTexture);", + " const texturesByImage = isNormalMap ? layerNormalMaps : layerTextures;", + " let texturesByAnisotropy = texturesByImage.get(imageTexture);", + " if (!texturesByAnisotropy) {", + " texturesByAnisotropy = new Map();", + " texturesByImage.set(imageTexture, texturesByAnisotropy);", + " }", + " let texture = texturesByAnisotropy.get(anisotropy);", " if (!texture) {", " texture = imageTexture.clone();", " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", @@ -39029,13 +39087,71 @@ " texture.wrapT = THREE.RepeatWrapping;", " texture.minFilter = THREE.LinearMipmapLinearFilter;", " texture.generateMipmaps = true;", - " texture.anisotropy = 4;", + " texture.anisotropy = anisotropy;", " texture.needsUpdate = true;", - " textures.set(imageTexture, texture);", + " texturesByAnisotropy.set(anisotropy, texture);", " }", " return texture;", "};", "", + "/** @type {Float32Array | null} */", + "let srgbToLinearValues = null;", + "/** @type {WeakMap} */", + "const linearAverageColors = new WeakMap();", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Vector3 | null} The average color of the texture, in the", + " * linear color space (like colors read by shaders), or null if the image", + " * can't be read: not loaded yet, or from another domain without CORS.", + " */", + "const getLinearAverageColor = (texture) => {", + " const image = texture.image;", + " if (!image) return null;", + " const cachedColor = linearAverageColors.get(image);", + " if (cachedColor) return cachedColor;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return null;", + " const size = 64;", + " let pixels;", + " try {", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " // Downscaled with filtering, so that all pixels are counted.", + " context.imageSmoothingEnabled = true;", + " context.imageSmoothingQuality = 'high';", + " context.drawImage(image, 0, 0, size, size);", + " pixels = context.getImageData(0, 0, size, size).data;", + " } catch (error) {", + " return null;", + " }", + " if (!srgbToLinearValues) {", + " srgbToLinearValues = new Float32Array(256);", + " for (let value = 0; value < 256; value++) {", + " const srgb = value / 255;", + " srgbToLinearValues[value] = srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);", + " }", + " }", + " const isSrgb = texture.colorSpace === THREE.SRGBColorSpace;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += isSrgb ? srgbToLinearValues[pixels[index]] : pixels[index] / 255;", + " green += isSrgb ? srgbToLinearValues[pixels[index + 1]] : pixels[index + 1] / 255;", + " blue += isSrgb ? srgbToLinearValues[pixels[index + 2]] : pixels[index + 2] / 255;", + " }", + " const count = size * size;", + " const color = new THREE.Vector3(red / count, green / count, blue / count);", + " linearAverageColors.set(image, color);", + " return color;", + "};", + "", "/**", " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", " * weights, tint and wetness...), interpolated between samples.", @@ -39094,11 +39210,14 @@ " terrainLayerMap1: { value: getWhiteTexture() },", " terrainLayerMap2: { value: getWhiteTexture() },", " terrainLayerMap3: { value: getWhiteTexture() },", + " // Used by the height blending, in the linear color space.", + " terrainLayerAverageColors: {", + " value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)],", + " },", " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", - " terrainHasNormalMaps: { value: 0 },", " terrainNormalMapStrength: { value: 1 },", " terrainTextureSize: { value: 256 },", " terrainHeightBlending: { value: 0 },", @@ -39107,6 +39226,7 @@ " terrainCliffCosines: { value: new THREE.Vector2() },", " terrainBrush: { value: new THREE.Vector4() },", " terrainColorMap: { value: getWhiteTexture() },", + " terrainNoiseMap: { value: getNoiseTexture() },", " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", " terrainWetShore: { value: new THREE.Vector3() },", " // Scene units by 3D world unit.", @@ -39126,8 +39246,12 @@ " ['#include ', '#include \\n' + fragmentShaderNormalCode],", " ], 'Terrain3D');", " };", - " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", " this.hasHoles = false;", + " /** The defines of the features used by the terrain (see `updateMaterial`), without holes. */", + " this.featureDefines = [];", + " /** All the defines of the shader, separated by semicolons. */", + " this.shaderKey = '';", + " this.material.customProgramCacheKey = () => 'Terrain3D;' + this.shaderKey;", " // Without them, holes would cast shadows.", " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", @@ -39164,11 +39288,28 @@ " setHasHoles(hasHoles) {", " if (hasHoles === this.hasHoles) return;", " this.hasHoles = hasHoles;", + " this._updateShaderDefines();", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /**", + " * Compile the shader only with the features used by the terrain. It is", + " * compiled again only when they change: properties only change in the", + " * editor, holes can be made by actions.", + " */", + " _updateShaderDefines() {", + " const defines = this.hasHoles ? [...this.featureDefines, 'TERRAIN_HOLES'] : this.featureDefines;", + " const shaderKey = defines.join(';');", + " if (shaderKey === this.shaderKey) return;", " // The material has its own defines (STANDARD).", - " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", - " else delete this.material.defines.TERRAIN_HOLES;", + " for (const name of this.shaderKey ? this.shaderKey.split(';') : []) {", + " delete this.material.defines[name];", + " }", + " for (const name of defines) {", + " this.material.defines[name] = '';", + " }", + " this.shaderKey = shaderKey;", " this.material.needsUpdate = true;", - " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", " }", "", " /** @param {THREE.Mesh} mesh */", @@ -39400,16 +39541,38 @@ " object._getLayer3NormalMap(),", " object._getLayer4NormalMap(),", " ];", + " const anisotropy = Math.max(parseInt(object._getAnisotropy(), 10) || DEFAULT_ANISOTROPY, 1);", + " const heightBlending = clamp01(object._getHeightBlending());", + " const textureVariation = clamp01(object._getTextureVariation());", + " const featureDefines = [];", " for (let layer = 0; layer < LAYER_COUNT; layer++) {", " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", - " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", - " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " const layerTexture = getLayerTexture(game, textures[layer], false, anisotropy);", + " this.uniforms['terrainLayerMap' + layer].value = layerTexture;", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true, anisotropy);", + " const averageColor = this.uniforms.terrainLayerAverageColors.value[layer];", + " if (textures[layer]) {", + " featureDefines.push('TERRAIN_LAYER_MAP_' + layer);", + " const linearAverageColor = heightBlending > 0 ? getLinearAverageColor(layerTexture) : null;", + " if (linearAverageColor) {", + " averageColor.copy(linearAverageColor);", + " } else if (heightBlending > 0) {", + " featureDefines.push('TERRAIN_LAYER_MIPMAP_AVERAGE_' + layer);", + " }", + " } else {", + " averageColor.set(1, 1, 1);", + " }", + " if (normalMaps[layer]) featureDefines.push('TERRAIN_LAYER_NORMAL_MAP_' + layer);", " }", - " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " if (normalMaps.some((normalMap) => !!normalMap)) featureDefines.push('TERRAIN_NORMAL_MAPS');", + " if (heightBlending > 0) featureDefines.push('TERRAIN_HEIGHT_BLENDING');", + " if (textureVariation > 0) featureDefines.push('TERRAIN_TEXTURE_VARIATION');", + " this.featureDefines = featureDefines;", + " this._updateShaderDefines();", " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", - " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", - " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + " this.uniforms.terrainHeightBlending.value = heightBlending;", + " this.uniforms.terrainTextureVariation.value = textureVariation;", "", " const cliffLayer = parseInt(object._getCliffLayer(), 10);", " const cliffAngle = gdjs.toRad(object._getCliffAngle());", @@ -40327,40 +40490,21 @@ " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", "};", "", + "/** @type {WeakMap} */", "const averageColors = new WeakMap();", "let white = null;", "/**", " * @param {THREE.Texture} texture", - " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " * @returns {THREE.Color} The average color of the texture (white when it can't be read).", " */", "const getAverageColor = (texture) => {", - " const cachedColor = averageColors.get(texture);", - " if (cachedColor) return cachedColor;", - " const image = texture.image;", - " const isDrawable =", - " typeof document !== 'undefined' &&", - " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", - " image instanceof HTMLCanvasElement ||", - " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", - " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", - " const size = 8;", - " const canvas = document.createElement('canvas');", - " canvas.width = size;", - " canvas.height = size;", - " const context = canvas.getContext('2d');", - " context.drawImage(image, 0, 0, size, size);", - " const pixels = context.getImageData(0, 0, size, size).data;", - " let red = 0;", - " let green = 0;", - " let blue = 0;", - " for (let index = 0; index < pixels.length; index += 4) {", - " red += pixels[index];", - " green += pixels[index + 1];", - " blue += pixels[index + 2];", + " const linearAverageColor = getLinearAverageColor(texture);", + " if (!linearAverageColor) return white || (white = new THREE.Color(1, 1, 1));", + " let color = averageColors.get(linearAverageColor);", + " if (!color) {", + " color = new THREE.Color(linearAverageColor.x, linearAverageColor.y, linearAverageColor.z);", + " averageColors.set(linearAverageColor, color);", " }", - " const count = size * size * 255;", - " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", - " averageColors.set(texture, color);", " return color;", "};", "", @@ -40666,14 +40810,14 @@ } ], "parameters": [], - "objectGroups": [], - "description": "Define helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define grass helper classes JavaScript code.", "fullName": "Define grass helper classes", "functionType": "Action", "name": "DefineGrassHelperClasses", + "private": true, "sentence": "Define grass helper classes JavaScript code", "events": [ { @@ -41011,18 +41155,18 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define grass helper classes JavaScript code.", - "private": true + "objectGroups": [] }, { + "description": "Define scattered models helper classes JavaScript code.", "fullName": "Define scattered models helper classes", "functionType": "Action", "name": "DefineScatterHelperClasses", + "private": true, "sentence": "Define scattered models helper classes JavaScript code", "events": [ { @@ -41254,18 +41398,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -41276,19 +41441,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -41317,17 +41485,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -41354,8 +41531,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -41449,11 +41629,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -41466,7 +41651,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -41494,14 +41680,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -41549,9 +41738,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -41570,6 +41764,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -41682,9 +41877,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -41739,12 +41934,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -41918,8 +42113,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -42216,163 +42414,413 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], - "objectGroups": [], - "description": "Define scattered models helper classes JavaScript code.", - "private": true - } - ], - "eventsBasedBehaviors": [], - "eventsBasedObjects": [ + "objectGroups": [] + }, { - "areaMaxX": 4096, - "areaMaxY": 4096, - "areaMaxZ": 600, - "areaMinX": 0, - "areaMinY": 0, - "areaMinZ": 0, - "defaultName": "Terrain", - "assetStoreTag": "3d terrains", - "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", - "fullName": "3D terrain", - "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", - "is3D": true, - "isUsingLegacyInstancesRenderer": true, - "name": "Terrain3D", - "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", - "objects": [ - { - "assetStoreId": "", - "name": "AreaPlaceholder", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "width": 4096, - "height": 4096, - "depth": 600, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "backFaceUpThroughWhichAxisRotation": "X", - "materialType": "Basic", - "tint": "255;255;255", - "frontFaceResourceName": "", - "frontFaceVisible": false, - "frontFaceResourceRepeat": false, - "backFaceResourceName": "", - "backFaceVisible": false, - "backFaceResourceRepeat": false, - "leftFaceResourceName": "", - "leftFaceVisible": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceName": "", - "rightFaceVisible": false, - "rightFaceResourceRepeat": false, - "topFaceResourceName": "", - "topFaceVisible": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceName": "", - "bottomFaceVisible": false, - "bottomFaceResourceRepeat": false - } - } - ], - "objectsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "objectName": "AreaPlaceholder" - } - ] - }, - "objectsGroups": [], - "layers": [ + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "camera2DPlaneMaxDrawingDistance": 5000, - "camera3DFarPlaneDistance": 10000, - "camera3DFieldOfView": 45, - "camera3DNearPlaneDistance": 3, - "cameraType": "", - "followBaseLayerCamera": false, - "isLightingLayer": false, - "isLocked": false, - "name": "", - "renderingType": "", - "visibility": true, - "cameras": [ + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 - } - ], - "effects": [] - } - ], - "instances": [ - { - "angle": 0, - "customSize": true, - "width": 4096, - "height": 4096, - "depth": 600, - "keepRatio": true, - "layer": "", - "name": "AreaPlaceholder", - "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", - "x": 0, - "y": 0, - "z": 0, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - } - ], - "editionSettings": {}, - "eventsFunctions": [ - { - "fullName": "", - "functionType": "Action", - "name": "onCreated", - "sentence": "", - "events": [ + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "Terrain3D::DefineHelperClasses" - }, - "parameters": [ - "", - "" - ] - } + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" ] }, { - "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": [ - "/** @type {gdjs.CustomRuntimeObject3D} */", - "const object = objects[0];", - "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", - " child.deleteFromScene();", - "}", - "if (object.__terrain3D) object.__terrain3D.dispose();", + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" ], "parameterObjects": "Object", @@ -42384,8 +42832,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -42412,8 +42860,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -42440,8 +42888,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] @@ -42468,15 +42916,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" } ], "objectGroups": [] }, { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", "fullName": "Raise or lower the ground", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGround", "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -42500,8 +42950,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -42515,9 +42965,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -42525,13 +42975,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", "fullName": "Raise or lower the ground along a line", "functionType": "Action", + "group": "Sculpt", "name": "RaiseGroundAlongLine", "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -42557,8 +43007,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -42582,9 +43032,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Height to add (negative to lower the ground)", @@ -42592,13 +43042,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Flatten the ground around a position to a given height, for example to place a building.", "fullName": "Flatten the ground", "functionType": "Action", + "group": "Sculpt", "name": "FlattenGround", "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -42622,8 +43072,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -42637,9 +43087,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Z position of the flat ground", @@ -42647,13 +43097,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Flatten the ground around a position to a given height, for example to place a building.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Smooth the ground around a position, softening bumps and cliffs.", "fullName": "Smooth the ground", "functionType": "Action", + "group": "Sculpt", "name": "SmoothGround", "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", "events": [ @@ -42677,8 +43127,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -42692,9 +43142,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Strength (between 0 and 1)", @@ -42702,13 +43152,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Smooth the ground around a position, softening bumps and cliffs.", - "group": "Sculpt" + "objectGroups": [] }, { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", "fullName": "Paint a layer", "functionType": "Action", + "group": "Paint", "name": "PaintLayer", "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -42732,8 +43182,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -42747,9 +43197,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -42757,13 +43207,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", "fullName": "Paint a layer along a line", "functionType": "Action", + "group": "Paint", "name": "PaintLayerAlongLine", "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", "events": [ @@ -42789,8 +43239,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the start", @@ -42814,9 +43264,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance from the line." + "type": "expression" }, { "description": "Layer (1 to 4)", @@ -42824,13 +43274,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint a layer on the ground along a line, for example for a path or a road.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", "fullName": "Tint the ground", "functionType": "Action", + "group": "Paint", "name": "TintGround", "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -42854,8 +43304,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -42869,9 +43319,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Color", @@ -42879,13 +43329,13 @@ "type": "color" } ], - "objectGroups": [], - "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", "fullName": "Change the wetness of the ground", "functionType": "Action", + "group": "Paint", "name": "WetGround", "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -42909,8 +43359,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -42924,9 +43374,9 @@ }, { "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", "name": "Radius", - "type": "expression", - "longDescription": "The effect fades out smoothly up to this distance." + "type": "expression" }, { "description": "Wetness (from 0 for dry to 1)", @@ -42934,13 +43384,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", "fullName": "Ground wetness", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundWetness", "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", "events": [ @@ -42958,12 +43408,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -42976,16 +43429,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", "fullName": "Ground tint", "functionType": "StringExpression", + "group": "Paint", "name": "GroundTint", "sentence": "", "events": [ @@ -43003,12 +43453,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "string" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -43021,16 +43474,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", - "group": "Paint", - "expressionType": { - "type": "string" - } + "objectGroups": [] }, { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", "fullName": "Make a hole", "functionType": "Action", + "group": "Holes", "name": "MakeHole", "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -43053,8 +43503,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -43072,13 +43522,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", "fullName": "Fill holes", "functionType": "Action", + "group": "Holes", "name": "FillHoles", "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -43101,8 +43551,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -43120,13 +43570,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", "fullName": "Carve an entrance", "functionType": "Action", + "group": "Holes", "name": "CarveEntrance", "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", "events": [ @@ -43152,8 +43602,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position of the center", @@ -43186,13 +43636,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", - "group": "Holes" + "objectGroups": [] }, { + "description": "Check if there is a hole in the ground at a position.", "fullName": "Hole", "functionType": "Condition", + "group": "Holes", "name": "IsHole", "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", "events": [ @@ -43214,8 +43664,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -43228,13 +43678,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Check if there is a hole in the ground at a position.", - "group": "Holes" + "objectGroups": [] }, { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", "fullName": "Ground Z position, with holes", "functionType": "Expression", + "group": "Holes", "name": "GroundZWithHoles", "sentence": "", "events": [ @@ -43253,12 +43703,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -43272,21 +43725,18 @@ }, { "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", "name": "ZInHoles", - "type": "expression", - "longDescription": "For example, a Z position far under the ground." + "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the ground at a position, or the given value where there is a hole.", - "group": "Holes", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "Replace the ground with a generated relief. Painted layers are reset.", "fullName": "Generate a relief", "functionType": "Action", + "group": "Generation", "name": "GenerateRelief", "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", "events": [ @@ -43308,29 +43758,29 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Relief", "name": "Relief", - "type": "stringWithSelector", - "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]" + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" }, { "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", "name": "Seed", - "type": "expression", - "longDescription": "Each seed gives a different terrain with the same relief." + "type": "expression" } ], - "objectGroups": [], - "description": "Replace the ground with a generated relief. Painted layers are reset.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", "fullName": "Load a heightmap", "functionType": "Action", + "group": "Generation", "name": "LoadHeightmap", "sentence": "Load the heights of _PARAM0_ from _PARAM1_", "events": [ @@ -43351,23 +43801,23 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", "name": "Image", - "type": "imageResource", - "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels." + "type": "imageResource" } ], - "objectGroups": [], - "description": "Replace the ground with the heights of an image. Painted layers are kept.", - "group": "Generation" + "objectGroups": [] }, { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", "fullName": "Put objects on the ground", "functionType": "Action", + "group": "Position", "name": "PutOnGround", "sentence": "Put _PARAM1_ on the ground of _PARAM0_", "events": [ @@ -43387,8 +43837,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "Objects to put on the ground", @@ -43396,13 +43846,13 @@ "type": "objectList" } ], - "objectGroups": [], - "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", - "group": "Position" + "objectGroups": [] }, { + "description": "the Z position of the ground at a position of the scene.", "fullName": "Ground Z position", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundZ", "sentence": "the ground Z position at _PARAM1_; _PARAM2_", "events": [ @@ -43420,12 +43870,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -43438,16 +43891,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the Z position of the ground at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", "fullName": "Ground slope", "functionType": "ExpressionAndCondition", + "group": "Position", "name": "GroundSlope", "sentence": "the ground slope at _PARAM1_; _PARAM2_", "events": [ @@ -43465,12 +43915,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -43483,16 +43936,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", - "group": "Position", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", "fullName": "Ground layer", "functionType": "ExpressionAndCondition", + "group": "Paint", "name": "GroundLayer", "sentence": "the ground layer at _PARAM1_; _PARAM2_", "events": [ @@ -43510,12 +43960,15 @@ "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::Terrain3D" + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" }, { "description": "X position", @@ -43528,14 +43981,114 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", - "group": "Paint", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "Hills", @@ -43580,6 +44133,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "HeightmapImage" }, { @@ -43637,6 +44191,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1Texture" }, { @@ -43648,6 +44203,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer1NormalMap" }, { @@ -43666,6 +44222,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2Texture" }, { @@ -43677,6 +44234,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer2NormalMap" }, { @@ -43695,6 +44253,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3Texture" }, { @@ -43706,6 +44265,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer3NormalMap" }, { @@ -43724,6 +44284,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4Texture" }, { @@ -43735,6 +44296,7 @@ "extraInformation": [ "image" ], + "choices": [], "name": "Layer4NormalMap" }, { @@ -43748,8 +44310,8 @@ { "value": "256", "type": "Number", - "label": "Texture size", "unit": "Pixel", + "label": "Texture size", "description": "The size, in the scene, of one repetition of the layer textures.", "group": "Layers", "name": "TextureSize" @@ -43829,8 +44391,8 @@ { "value": "45", "type": "Number", - "label": "Steep slope angle", "unit": "DegreeAngle", + "label": "Steep slope angle", "group": "Layers", "name": "CliffAngle" }, @@ -43848,6 +44410,37 @@ "group": "Rendering", "name": "ReceiveShadow" }, + { + "value": "4", + "type": "Choice", + "label": "Anisotropic filtering", + "description": "Keeps layer textures sharp where the ground is seen at a low angle (like from a car). Lower is faster on slow graphics cards, but blurs the ground in the distance.", + "group": "Rendering", + "choices": [ + { + "label": "None (fastest)", + "value": "1" + }, + { + "label": "2x", + "value": "2" + }, + { + "label": "4x", + "value": "4" + }, + { + "label": "8x", + "value": "8" + }, + { + "label": "16x (sharpest)", + "value": "16" + } + ], + "advanced": true, + "name": "Anisotropy" + }, { "value": "", "type": "String", @@ -43856,7 +44449,123 @@ "advanced": true, "name": "SculptData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + }, + { + "propertyName": "Anisotropy" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] }, { "areaMaxX": 1000, @@ -43868,6 +44577,7 @@ "defaultName": "Grass", "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", "fullName": "Terrain grass", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -43955,16 +44665,15 @@ { "angle": 0, "customSize": true, - "width": 1000, - "height": 1000, "depth": 40, + "height": 1000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -44029,8 +44738,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -44057,8 +44766,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -44093,8 +44802,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] @@ -44121,13 +44830,30 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::TerrainGrass" + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" } ], "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, "propertyDescriptors": [ { "value": "60", @@ -44140,8 +44866,8 @@ { "value": "36", "type": "Number", - "label": "Blade height", "unit": "Pixel", + "label": "Blade height", "description": "The average height of blades (each blade is between 60% and 140% of it).", "group": "Blades", "name": "BladeHeight" @@ -44149,8 +44875,8 @@ { "value": "5", "type": "Number", - "label": "Blade width", "unit": "Pixel", + "label": "Blade width", "group": "Blades", "name": "BladeWidth" }, @@ -44209,8 +44935,8 @@ { "value": "40", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No blades grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -44241,13 +44967,75 @@ { "value": "2500", "type": "Number", - "label": "Fade distance", "unit": "Pixel", + "label": "Fade distance", "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", "group": "Rendering", "name": "FadeDistance" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] }, { "areaMaxX": 2000, @@ -44256,10 +45044,11 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, - "defaultName": "ScatteredModels", "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", "is3D": true, "isUsingLegacyInstancesRenderer": true, @@ -44269,6 +45058,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -44347,23 +45137,38 @@ { "angle": 0, "customSize": true, - "width": 2000, - "height": 2000, "depth": 200, + "height": 2000, "keepRatio": true, "layer": "", "name": "AreaPlaceholder", "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, "x": 0, "y": 0, - "z": 0, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -44421,8 +45226,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -44449,8 +45254,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -44481,8 +45286,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] @@ -44509,15 +45314,17 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" } ], "objectGroups": [] }, { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", "fullName": "Add copies", "functionType": "Action", + "group": "Paint", "name": "AddCopies", "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -44540,8 +45347,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -44559,13 +45366,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", - "group": "Paint" + "objectGroups": [] }, { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", "fullName": "Erase copies", "functionType": "Action", + "group": "Paint", "name": "EraseCopies", "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", "events": [ @@ -44588,8 +45395,8 @@ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position of the center", @@ -44607,13 +45414,13 @@ "type": "expression" } ], - "objectGroups": [], - "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", - "group": "Paint" + "objectGroups": [] }, { + "description": "the number of copies placed in the area.", "fullName": "Number of copies", "functionType": "ExpressionAndCondition", + "group": "Copies", "name": "CopyCount", "sentence": "the number of copies", "events": [ @@ -44622,32 +45429,124 @@ "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", "const object = objects[0];", - "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" } ], - "objectGroups": [], - "description": "the number of copies placed in the area.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "X position of the nearest copy", + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", "functionType": "Expression", - "name": "NearestCopyX", + "group": "Copies", + "name": "NearestCopyZ", "sentence": "", "events": [ { @@ -44658,19 +45557,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.x : x;" + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -44683,18 +45585,15 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Y position of the nearest copy", - "functionType": "Expression", - "name": "NearestCopyY", - "sentence": "", + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", "events": [ { "type": "BuiltinCommonInstructions::JsCode", @@ -44704,19 +45603,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.y : y;" + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -44729,20 +45631,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Z position of the nearest copy", + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", "functionType": "Expression", - "name": "NearestCopyZ", + "group": "Copies", + "name": "NearestCopyAngle", "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -44750,19 +45650,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -44775,20 +45678,18 @@ "type": "expression" } ], - "objectGroups": [], - "description": "The Z position of the base of the copy nearest to a position (on the ground).", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] }, { - "fullName": "Distance to the nearest copy", - "functionType": "ExpressionAndCondition", - "name": "NearestCopyDistance", - "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", "events": [ { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ "/** @type {gdjs.CustomRuntimeObject3D} */", @@ -44796,19 +45697,22 @@ "const x = eventsFunctionContext.getArgument(\"X\");", "const y = eventsFunctionContext.getArgument(\"Y\");", "const copy = object.__terrainScatter.findNearestCopy(x, y);", - "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { "description": "Object", "name": "Object", - "type": "object", - "supplementaryInformation": "Terrain3D::ScatteredModels" + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" }, { "description": "X position", @@ -44821,33 +45725,129 @@ "type": "expression" } ], - "objectGroups": [], - "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", - "group": "Copies", - "expressionType": { - "type": "expression" - } + "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" ], + "choices": [], "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", - "label": "Height of a copy", "unit": "Pixel", - "description": "Copies keep the proportions of the model.", + "label": "Height of a copy", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -44948,8 +45948,8 @@ { "value": "30", "type": "Number", - "label": "Maximum slope", "unit": "DegreeAngle", + "label": "Maximum slope", "description": "No copies grow on steeper ground.", "group": "Ground", "name": "MaxSlope" @@ -44957,8 +45957,8 @@ { "value": "-100000", "type": "Number", - "label": "Minimum ground Z", "unit": "Pixel", + "label": "Minimum ground Z", "description": "No copies grow where the ground is lower (for example, under the water level).", "group": "Ground", "name": "MinGroundZ" @@ -44966,8 +45966,8 @@ { "value": "100000", "type": "Number", - "label": "Maximum ground Z", "unit": "Pixel", + "label": "Maximum ground Z", "description": "No copies grow where the ground is higher (for example, above the tree line).", "group": "Ground", "name": "MaxGroundZ" @@ -44983,17 +45983,17 @@ { "value": "30", "type": "Number", - "label": "Trunk diameter", "unit": "Pixel", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, { "value": "8000", "type": "Number", - "label": "Draw distance", "unit": "Pixel", + "label": "Draw distance", "description": "Copies shrink and are hidden at this distance from the camera.", "group": "Rendering", "name": "DrawDistance" @@ -45029,7 +46029,107 @@ "advanced": true, "name": "PaintData" } - ] + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + }, + { + "propertyName": "Size" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] } diff --git a/examples/starting-first-person-farming/starting-first-person-farming.json b/examples/starting-first-person-farming/starting-first-person-farming.json index 6bd6a123f..70064b2dc 100644 --- a/examples/starting-first-person-farming/starting-first-person-farming.json +++ b/examples/starting-first-person-farming/starting-first-person-farming.json @@ -48372,7 +48372,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.2.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -48408,6 +48408,12 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], @@ -51812,7 +51818,7 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -52055,18 +52061,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -52077,19 +52104,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -52118,17 +52148,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -52155,8 +52194,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -52250,11 +52292,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -52267,7 +52314,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -52295,14 +52343,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -52350,9 +52401,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -52371,6 +52427,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -52483,9 +52540,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -52540,12 +52597,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -52719,8 +52776,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -53017,11 +53077,246 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -53035,6 +53330,9 @@ }, { "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" } ] }, @@ -55423,6 +55721,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -55516,7 +55815,23 @@ "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -55909,7 +56224,7 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "expressionType": { @@ -55980,6 +56295,100 @@ } ], "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -56025,6 +56434,12 @@ }, { "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" } ] } @@ -56035,7 +56450,7 @@ "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" @@ -56044,12 +56459,58 @@ "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", "unit": "Pixel", "label": "Height of a copy", - "description": "Copies keep the proportions of the model.", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -56187,7 +56648,7 @@ "type": "Number", "unit": "Pixel", "label": "Trunk diameter", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, @@ -56242,7 +56703,13 @@ "propertyName": "Model" }, { - "propertyName": "Size" + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" }, { "propertyName": "MinScale" @@ -56255,6 +56722,9 @@ }, { "propertyName": "RandomTint" + }, + { + "propertyName": "Size" } ] }, diff --git a/examples/starting-first-person-horror/starting-first-person-horror.json b/examples/starting-first-person-horror/starting-first-person-horror.json index 9a13166a8..971ccca6e 100644 --- a/examples/starting-first-person-horror/starting-first-person-horror.json +++ b/examples/starting-first-person-horror/starting-first-person-horror.json @@ -30027,7 +30027,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.2.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -30063,6 +30063,12 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], @@ -33467,7 +33473,7 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -33710,18 +33716,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -33732,19 +33759,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -33773,17 +33803,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -33810,8 +33849,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -33905,11 +33947,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -33922,7 +33969,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -33950,14 +33998,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -34005,9 +34056,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -34026,6 +34082,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -34138,9 +34195,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -34195,12 +34252,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -34374,8 +34431,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -34672,11 +34732,246 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -34690,6 +34985,9 @@ }, { "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" } ] }, @@ -37078,6 +37376,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -37171,7 +37470,23 @@ "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -37564,7 +37879,7 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "expressionType": { @@ -37635,6 +37950,100 @@ } ], "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -37680,6 +38089,12 @@ }, { "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" } ] } @@ -37690,7 +38105,7 @@ "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" @@ -37699,12 +38114,58 @@ "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", "unit": "Pixel", "label": "Height of a copy", - "description": "Copies keep the proportions of the model.", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -37842,7 +38303,7 @@ "type": "Number", "unit": "Pixel", "label": "Trunk diameter", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, @@ -37897,7 +38358,13 @@ "propertyName": "Model" }, { - "propertyName": "Size" + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" }, { "propertyName": "MinScale" @@ -37910,6 +38377,9 @@ }, { "propertyName": "RandomTint" + }, + { + "propertyName": "Size" } ] }, diff --git a/examples/starting-first-person-shooter-enemies/starting-first-person-shooter-enemies.json b/examples/starting-first-person-shooter-enemies/starting-first-person-shooter-enemies.json index 92af97702..3a32cb3fc 100644 --- a/examples/starting-first-person-shooter-enemies/starting-first-person-shooter-enemies.json +++ b/examples/starting-first-person-shooter-enemies/starting-first-person-shooter-enemies.json @@ -41266,7 +41266,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.2.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -41302,6 +41302,12 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], @@ -44706,7 +44712,7 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -44949,18 +44955,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -44971,19 +44998,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -45012,17 +45042,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -45049,8 +45088,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -45144,11 +45186,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -45161,7 +45208,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -45189,14 +45237,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -45244,9 +45295,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -45265,6 +45321,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -45377,9 +45434,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -45434,12 +45491,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -45613,8 +45670,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -45911,11 +45971,246 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -45929,6 +46224,9 @@ }, { "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" } ] }, @@ -48317,6 +48615,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -48410,7 +48709,23 @@ "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -48803,7 +49118,7 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "expressionType": { @@ -48874,6 +49189,100 @@ } ], "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -48919,6 +49328,12 @@ }, { "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" } ] } @@ -48929,7 +49344,7 @@ "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" @@ -48938,12 +49353,58 @@ "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", "unit": "Pixel", "label": "Height of a copy", - "description": "Copies keep the proportions of the model.", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -49081,7 +49542,7 @@ "type": "Number", "unit": "Pixel", "label": "Trunk diameter", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, @@ -49136,7 +49597,13 @@ "propertyName": "Model" }, { - "propertyName": "Size" + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" }, { "propertyName": "MinScale" @@ -49149,6 +49616,9 @@ }, { "propertyName": "RandomTint" + }, + { + "propertyName": "Size" } ] }, diff --git a/examples/starting-first-person-shooter-horror/starting-first-person-shooter-horror.json b/examples/starting-first-person-shooter-horror/starting-first-person-shooter-horror.json index cf8167dec..e19a24b0e 100644 --- a/examples/starting-first-person-shooter-horror/starting-first-person-shooter-horror.json +++ b/examples/starting-first-person-shooter-horror/starting-first-person-shooter-horror.json @@ -46044,7 +46044,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.2.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -46080,6 +46080,12 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], @@ -49484,7 +49490,7 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -49727,18 +49733,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -49749,19 +49776,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -49790,17 +49820,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -49827,8 +49866,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -49922,11 +49964,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -49939,7 +49986,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -49967,14 +50015,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -50022,9 +50073,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -50043,6 +50099,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -50155,9 +50212,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -50212,12 +50269,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -50391,8 +50448,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -50689,11 +50749,246 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -50707,6 +51002,9 @@ }, { "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" } ] }, @@ -53095,6 +53393,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -53188,7 +53487,23 @@ "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -53581,7 +53896,7 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "expressionType": { @@ -53652,6 +53967,100 @@ } ], "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -53697,6 +54106,12 @@ }, { "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" } ] } @@ -53707,7 +54122,7 @@ "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" @@ -53716,12 +54131,58 @@ "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", "unit": "Pixel", "label": "Height of a copy", - "description": "Copies keep the proportions of the model.", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -53859,7 +54320,7 @@ "type": "Number", "unit": "Pixel", "label": "Trunk diameter", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, @@ -53914,7 +54375,13 @@ "propertyName": "Model" }, { - "propertyName": "Size" + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" }, { "propertyName": "MinScale" @@ -53927,6 +54394,9 @@ }, { "propertyName": "RandomTint" + }, + { + "propertyName": "Size" } ] }, diff --git a/examples/starting-first-person-shooter/starting-first-person-shooter.json b/examples/starting-first-person-shooter/starting-first-person-shooter.json index 05a010415..daf429722 100644 --- a/examples/starting-first-person-shooter/starting-first-person-shooter.json +++ b/examples/starting-first-person-shooter/starting-first-person-shooter.json @@ -40053,7 +40053,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.2.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -40089,6 +40089,12 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], @@ -43493,7 +43499,7 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -43736,18 +43742,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -43758,19 +43785,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -43799,17 +43829,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -43836,8 +43875,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -43931,11 +43973,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -43948,7 +43995,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -43976,14 +44024,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -44031,9 +44082,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -44052,6 +44108,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -44164,9 +44221,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -44221,12 +44278,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -44400,8 +44457,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -44698,11 +44758,246 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -44716,6 +45011,9 @@ }, { "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" } ] }, @@ -47104,6 +47402,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -47197,7 +47496,23 @@ "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -47590,7 +47905,7 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "expressionType": { @@ -47661,6 +47976,100 @@ } ], "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -47706,6 +48115,12 @@ }, { "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" } ] } @@ -47716,7 +48131,7 @@ "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" @@ -47725,12 +48140,58 @@ "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", "unit": "Pixel", "label": "Height of a copy", - "description": "Copies keep the proportions of the model.", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -47868,7 +48329,7 @@ "type": "Number", "unit": "Pixel", "label": "Trunk diameter", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, @@ -47923,7 +48384,13 @@ "propertyName": "Model" }, { - "propertyName": "Size" + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" }, { "propertyName": "MinScale" @@ -47936,6 +48403,9 @@ }, { "propertyName": "RandomTint" + }, + { + "propertyName": "Size" } ] }, diff --git a/examples/starting-first-person-survival-crafting/starting-first-person-survival-crafting.json b/examples/starting-first-person-survival-crafting/starting-first-person-survival-crafting.json index 503f431ed..fadc36e89 100644 --- a/examples/starting-first-person-survival-crafting/starting-first-person-survival-crafting.json +++ b/examples/starting-first-person-survival-crafting/starting-first-person-survival-crafting.json @@ -35562,7 +35562,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.2.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -35598,6 +35598,12 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], @@ -39002,7 +39008,7 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -39245,18 +39251,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -39267,19 +39294,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -39308,17 +39338,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -39345,8 +39384,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -39440,11 +39482,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -39457,7 +39504,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -39485,14 +39533,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -39540,9 +39591,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -39561,6 +39617,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -39673,9 +39730,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -39730,12 +39787,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -39909,8 +39966,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -40207,11 +40267,246 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -40225,6 +40520,9 @@ }, { "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" } ] }, @@ -42613,6 +42911,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -42706,7 +43005,23 @@ "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -43099,7 +43414,7 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "expressionType": { @@ -43170,6 +43485,100 @@ } ], "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -43215,6 +43624,12 @@ }, { "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" } ] } @@ -43225,7 +43640,7 @@ "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" @@ -43234,12 +43649,58 @@ "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", "unit": "Pixel", "label": "Height of a copy", - "description": "Copies keep the proportions of the model.", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -43377,7 +43838,7 @@ "type": "Number", "unit": "Pixel", "label": "Trunk diameter", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, @@ -43432,7 +43893,13 @@ "propertyName": "Model" }, { - "propertyName": "Size" + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" }, { "propertyName": "MinScale" @@ -43445,6 +43912,9 @@ }, { "propertyName": "RandomTint" + }, + { + "propertyName": "Size" } ] }, diff --git a/examples/starting-first-person/starting-first-person.json b/examples/starting-first-person/starting-first-person.json index 82247f16d..3cc71369c 100644 --- a/examples/starting-first-person/starting-first-person.json +++ b/examples/starting-first-person/starting-first-person.json @@ -23187,7 +23187,7 @@ "name": "Terrain3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", - "version": "0.2.0", + "version": "0.3.1", "description": [ "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", "", @@ -23223,6 +23223,12 @@ "cave" ], "authorIds": [], + "changelog": [ + { + "version": "0.3.1", + "breaking": "- Scattered models now put the origin of their 3D model on the ground (\"Point put on the ground\" property), instead of the center of its base. Copies of models whose origin is elsewhere, like some trees of the asset store, can move a bit. Set \"Point put on the ground\" to \"Center of the model base\" to keep their previous placement." + } + ], "dependencies": [], "globalVariables": [], "sceneVariables": [], @@ -26627,7 +26633,7 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -26870,18 +26876,39 @@ "};", "", "/**", + " * The parts of a model and its size, per unit of height, around the point", + " * of the model put on the ground (its anchor).", + " * @typedef {{", + " * parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>,", + " * width: number,", + " * depth: number,", + " * bottom: number,", + " * top: number,", + " * reach: number,", + " * }} ModelParts", + " * `bottom` and `top` are the Z of the model base and top from the anchor.", + " * `reach` is the farthest distance of the model from the anchor on X and Y,", + " * whatever the rotation of the copy.", + " */", + "", + "/** @type {ModelParts} */", + "const emptyModelParts = { parts: [], width: 0, depth: 0, bottom: 0, top: 1, reach: 0, sourceHeight: 1 };", + "", + "/**", " * The parts of a model, ready to be drawn with instancing: each glTF", - " * primitive with its material. Geometries are moved so that the model base", - " * is at the origin, its height is 1 and it stands along Z (like 3D model", - " * objects with a 90° rotation around X).", + " * primitive with its material. Geometries are moved so that the anchor is at", + " * the origin, the model height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X, for models standing on Y).", " * @param {THREE.Object3D} modelScene", " * @param {object} uniforms Uniforms added to the materials.", - " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " * @param {{isStandingOnZ: boolean, isAnchoredAtOrigin: boolean}} options", + " * The anchor is the origin of the model, or else the center of its base.", + " * @returns {ModelParts}", " */", - "const createModelParts = (modelScene, uniforms) => {", + "const createModelParts = (modelScene, uniforms, { isStandingOnZ, isAnchoredAtOrigin }) => {", " modelScene.updateMatrixWorld(true);", " const rootInverse = modelScene.matrixWorld.clone().invert();", - " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " const standUp = isStandingOnZ ? new THREE.Matrix4() : new THREE.Matrix4().makeRotationX(Math.PI / 2);", " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", " const meshes = [];", " const box = new THREE.Box3();", @@ -26892,19 +26919,22 @@ " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", " meshes.push({ mesh: node, matrix });", " });", - " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + " if (box.isEmpty()) return emptyModelParts;", "", " const height = Math.max(box.max.z - box.min.z, 0.000001);", " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", " const normalization = new THREE.Matrix4()", " .makeScale(1 / height, -1 / height, 1 / height)", " .multiply(", - " new THREE.Matrix4().makeTranslation(", - " -(box.min.x + box.max.x) / 2,", - " -(box.min.y + box.max.y) / 2,", - " -box.min.z", - " )", + " isAnchoredAtOrigin", + " ? new THREE.Matrix4()", + " : new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", " );", + " const normalizedBox = box.clone().applyMatrix4(normalization);", " const parts = meshes.map(({ mesh, matrix }) => {", " let geometry = mesh.geometry.clone();", " if (!geometry.index) {", @@ -26933,17 +26963,26 @@ " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", " return { geometry, material };", " });", + " // The sizes are relative to a height of 1, except sourceHeight which is in the model file units.", " return {", " parts,", + " sourceHeight: height,", " width: (box.max.x - box.min.x) / height,", " depth: (box.max.y - box.min.y) / height,", + " bottom: normalizedBox.min.z,", + " top: normalizedBox.max.z,", + " reach: Math.hypot(", + " Math.max(-normalizedBox.min.x, normalizedBox.max.x),", + " Math.max(-normalizedBox.min.y, normalizedBox.max.y)", + " ),", " };", "};", "", "/**", - " * A copy of the model: its base position in the scene, its scale (the", - " * height of the copy) and the key of its cell in the grid of copies.", - " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " * A copy of the model: the position of its anchor in the scene (on the ground), its scale", + " * factor (1 = the model at its original size) and rendered height in pixels, and the key", + " * of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, scale: number, key: number}} Copy", " */", "", "/**", @@ -26970,8 +27009,11 @@ " scatterDrawDistance: { value: 1 },", " };", " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " this.modelResourceName = '';", + " /** The properties the model was loaded from (see `_getModelKey`). */", + " this.modelKey = '';", " this.loadingModelResourceName = '';", " /**", " * Copies removed by events (for example, cut trees), by key.", @@ -27065,11 +27107,16 @@ " return this.paintData;", " }", "", - " updateFromProperties() {", + " _getModelKey() {", " const { object } = this;", - " const modelResourceName = object._getModel();", - " if (modelResourceName !== this.modelResourceName) {", - " this.modelResourceName = modelResourceName;", + " return object._getModel() + '|' + object._getModelUpAxis() + '|' + object._getModelAnchor();", + " }", + "", + " updateFromProperties() {", + " const modelKey = this._getModelKey();", + " if (modelKey !== this.modelKey) {", + " this.modelKey = modelKey;", + " this.modelResourceName = this.object._getModel();", " this._loadModel();", " }", " this.placement.placeAgain();", @@ -27082,7 +27129,8 @@ " material.dispose();", " }", " }", - " this.model = { parts: [], width: 0, depth: 0 };", + " /** @type {ModelParts} */", + " this.model = emptyModelParts;", " }", "", " _loadModel() {", @@ -27110,14 +27158,17 @@ " .catch(() => {});", " return;", " }", - " this.model = createModelParts(gltf.scene, this.uniforms);", + " this.model = createModelParts(gltf.scene, this.uniforms, {", + " isStandingOnZ: this.object._getModelUpAxis() === 'Z',", + " isAnchoredAtOrigin: this.object._getModelAnchor() === 'ModelOrigin',", + " });", " }", "", " _getPlacementKey() {", " const { object } = this;", " return [", " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", - " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getDensity(), object._getSize(), object._getScaleFactor(), object._getMinScale(), object._getMaxScale(),", " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", @@ -27165,9 +27216,14 @@ " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", " const groundZ = ground ? z : object.getZ();", " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", - " const copyHeight =", - " Math.max(object._getSize(), 0) *", - " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " const randomScale = object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor;", + " const sourceHeightInPixels = this.model.sourceHeight * 100;", + " // Objects made before \"Scale factor of a copy\" (store prefabs included) scale their fixed", + " // height (deprecated \"Size\" property) instead of the model height: they look the same at 100%.", + " const legacyHeight = object._getSize();", + " const baseHeight = legacyHeight > 0 ? legacyHeight : sourceHeightInPixels;", + " const copyHeight = (Math.max(object._getScaleFactor(), 0) / 100) * randomScale * baseHeight;", + " const copyScale = copyHeight / sourceHeightInPixels;", " if (ground) {", " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", @@ -27186,6 +27242,7 @@ " z: groundZ,", " angle: object._getRandomRotation() ? angle : 0,", " height: copyHeight,", + " scale: copyScale,", " key,", " };", " }", @@ -27298,9 +27355,9 @@ " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", " this._color.toArray(colors, index * 3);", " }", - " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", - " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", - " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " const reach = copy.height * this.model.reach;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z + copy.height * this.model.bottom));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height * this.model.top));", " });", " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", @@ -27355,12 +27412,12 @@ " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", " ray.direction.multiply(scale);", "", - " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const { reach: reachPerHeight, bottom, top } = this.model;", " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", " chunk.copies.forEach((copy, index) => {", " const reach = copy.height * reachPerHeight;", - " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", - " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z + copy.height * bottom);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height * top);", " if (!ray.intersectsBox(this._raycastBox)) return;", " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", " for (const mesh of chunk.meshes) {", @@ -27534,8 +27591,11 @@ " const depth = object.getDepth() || 1;", " let indexOffset = 0;", " copies.forEach((copy, copyIndex) => {", - " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", - " const top = copy.z + copy.height * 0.8;", + " const legacyHeight = object._getSize();", + " const trunkScale = legacyHeight > 0 ? copy.height / legacyHeight : copy.scale;", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * trunkScale;", + " // Up to 80% of the model height, at least a bit above the ground.", + " const top = copy.z + copy.height * Math.max(this.model.bottom + 0.8, 0.1);", " const firstVertex = copyIndex * verticesPerCopy;", " const setVertex = (vertex, x, y, z) => {", " positions[vertex * 3] = (x - object.getX()) / width;", @@ -27832,11 +27892,246 @@ ], "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] + }, + { + "description": "Create an object at the position, angle and scale of the copy of scattered models nearest to a position, and optionally erase that copy. The created object stays picked, so you apply actions to it right after.", + "fullName": "Replace the nearest copy with an object", + "functionType": "Action", + "name": "ReplaceNearestCopyWithObject", + "sentence": "Replace the copy of _PARAM1_ nearest to _PARAM5_; _PARAM6_ with _PARAM2_ (erase the copy: _PARAM7_)", + "events": [ + { + "aiGeneratedEventId": "01M6WAPE2W8KS8P2871Y641HST", + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyX", + "=", + "ScatterObject.NearestCopyX(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyY", + "=", + "ScatterObject.NearestCopyY(X, Y)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyZ", + "=", + "ScatterObject.NearestCopyZ(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyAngle", + "=", + "ScatterObject.NearestCopyAngle(CopyX, CopyY)" + ] + }, + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "CopyScale", + "=", + "ScatterObject.NearestCopyScale(CopyX, CopyY)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NumberVariable" + }, + "parameters": [ + "CopyScale", + ">", + "0" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BooleanVariable" + }, + "parameters": [ + "EraseTheCopy", + "True", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "Terrain3D::ScatteredModels::EraseCopies" + }, + "parameters": [ + "ScatterObject", + "CopyX", + "CopyY", + "1", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "ObjectToCreate", + "CopyX", + "CopyY", + "ScatterObject.Layer()" + ] + }, + { + "type": { + "value": "Scene3D::Base3DBehavior::SetZ" + }, + "parameters": [ + "ObjectToCreate", + "Object3DBehavior", + "=", + "CopyZ" + ] + }, + { + "type": { + "value": "SetAngle" + }, + "parameters": [ + "ObjectToCreate", + "=", + "CopyAngle" + ] + }, + { + "type": { + "value": "ScalableCapability::ScalableBehavior::SetValue" + }, + "parameters": [ + "ObjectToCreate", + "ScaleBehavior", + "=", + "CopyScale" + ] + } + ] + } + ] + } + ], + "variables": [ + { + "name": "CopyX", + "type": "number", + "value": 0 + }, + { + "name": "CopyY", + "type": "number", + "value": 0 + }, + { + "name": "CopyZ", + "type": "number", + "value": 0 + }, + { + "name": "CopyAngle", + "type": "number", + "value": 0 + }, + { + "name": "CopyScale", + "type": "number", + "value": 0 + } + ] + } + ], + "parameters": [ + { + "description": "Scattered models", + "name": "ScatterObject", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "objectList" + }, + { + "description": "Object to create", + "name": "ObjectToCreate", + "type": "objectListOrEmptyIfJustDeclared" + }, + { + "description": "3D behavior", + "name": "Object3DBehavior", + "supplementaryInformation": "Scene3D::Base3DBehavior", + "type": "behavior" + }, + { + "description": "Scale behavior", + "name": "ScaleBehavior", + "supplementaryInformation": "ScalableCapability::ScalableBehavior", + "type": "behavior" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Erase the copy", + "name": "EraseTheCopy", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -27850,6 +28145,9 @@ }, { "functionName": "DefineScatterHelperClasses" + }, + { + "functionName": "ReplaceNearestCopyWithObject" } ] }, @@ -30238,6 +30536,7 @@ { "assetStoreId": "", "name": "AreaPlaceholder", + "persistentUuid": "06ba5ea9-2e29-4867-90ba-dd3268867171", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], @@ -30331,7 +30630,23 @@ "initialVariables": [] } ], - "editionSettings": {}, + "editionSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridDepth": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridOffsetZ": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.35, + "windowMask": false, + "selectedLayer": "", + "gameEditorMode": "instances-editor" + }, "eventsFunctions": [ { "fullName": "", @@ -30724,7 +31039,7 @@ ], "parameterObjects": "Object", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "expressionType": { @@ -30795,6 +31110,100 @@ } ], "objectGroups": [] + }, + { + "description": "The angle (in degrees, around the Z axis) of the copy nearest to a position (0 when there is no copy).", + "fullName": "Angle of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyAngle", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W13A1GCBRR93J2V6ZPRBYZ", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? (copy.angle * 180) / Math.PI : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The scale factor of the copy nearest to a position (1 = the model at its original size, 0 when there is no copy).", + "fullName": "Scale of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyScale", + "sentence": "", + "events": [ + { + "aiGeneratedEventId": "01M6W15Y2Y5KXQHWMS0ZX3RSMT", + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.scale : 0;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] } ], "eventsFunctionsFolderStructure": { @@ -30840,6 +31249,12 @@ }, { "functionName": "NearestCopyDistance" + }, + { + "functionName": "NearestCopyAngle" + }, + { + "functionName": "NearestCopyScale" } ] } @@ -30850,7 +31265,7 @@ "value": "", "type": "Resource", "label": "3D model", - "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "description": "The model copied everywhere (a .glb file).", "group": "Model", "extraInformation": [ "model3D" @@ -30859,12 +31274,58 @@ "name": "Model" }, { - "value": "300", + "value": "Y", + "type": "Choice", + "label": "Model up axis", + "description": "The axis the model stands on in the file. Most models of the asset store stand on Y, they are stood up along Z.", + "group": "Model", + "choices": [ + { + "label": "Y (like most models)", + "value": "Y" + }, + { + "label": "Z (like the scene)", + "value": "Z" + } + ], + "name": "ModelUpAxis" + }, + { + "value": "ModelOrigin", + "type": "Choice", + "label": "Point put on the ground", + "description": "The point of the model put on the ground, also at the center of the trunk collision. Use the origin of the model when the trunk is not at the center of the model (asymmetric canopy, leaning tree), or to sink the base of copies in the ground on slopes.", + "group": "Model", + "choices": [ + { + "label": "Center of the model base", + "value": "BaseCenter" + }, + { + "label": "Origin of the model", + "value": "ModelOrigin" + } + ], + "name": "ModelAnchor" + }, + { + "value": "100", + "type": "Number", + "unit": "Dimensionless", + "label": "Scale factor of a copy", + "description": "The size of a copy, in percent of the original model size (100 = the model at its original size, like a 3D model object with a scale of 1). For objects made before this property, like the ones of the asset store, it is a percent of their former height of a copy. Copies keep the proportions of the model.", + "group": "Model", + "name": "ScaleFactor" + }, + { + "value": "0", "type": "Number", "unit": "Pixel", "label": "Height of a copy", - "description": "Copies keep the proportions of the model.", + "description": "Kept for objects made before \"Scale factor of a copy\": when greater than 0, copies have this height, multiplied by the scale factor.", "group": "Model", + "deprecated": true, "name": "Size" }, { @@ -31002,7 +31463,7 @@ "type": "Number", "unit": "Pixel", "label": "Trunk diameter", - "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "description": "The diameter of the collision shape of a copy at the scale given in \"Scale factor of a copy\" (it is scaled with the copy).", "group": "Collisions", "name": "TrunkDiameter" }, @@ -31057,7 +31518,13 @@ "propertyName": "Model" }, { - "propertyName": "Size" + "propertyName": "ModelUpAxis" + }, + { + "propertyName": "ModelAnchor" + }, + { + "propertyName": "ScaleFactor" }, { "propertyName": "MinScale" @@ -31070,6 +31537,9 @@ }, { "propertyName": "RandomTint" + }, + { + "propertyName": "Size" } ] }, diff --git a/rendering-screenshots/3d-island-terrain/the-island-from-above.png b/rendering-screenshots/3d-island-terrain/the-island-from-above.png index 43ae832c4..b8cf2fda0 100644 Binary files a/rendering-screenshots/3d-island-terrain/the-island-from-above.png and b/rendering-screenshots/3d-island-terrain/the-island-from-above.png differ diff --git a/rendering-screenshots/3d-island-terrain/the-view-of-the-player.png b/rendering-screenshots/3d-island-terrain/the-view-of-the-player.png index 70de1c621..b975f6948 100644 Binary files a/rendering-screenshots/3d-island-terrain/the-view-of-the-player.png and b/rendering-screenshots/3d-island-terrain/the-view-of-the-player.png differ