diff options
author | onefang | 2020-09-10 21:20:23 +1000 |
---|---|---|
committer | onefang | 2020-09-10 21:20:23 +1000 |
commit | 2940f325436f5b07dcfbac03545b3eb96e20ffcf (patch) | |
tree | 71babe10d710dac41431d20c8a22387d48f5130b /addon-modules/OpenSim.Modules.Warp3DCachedImageModule/src/TerrainSplat.cs | |
parent | Another ini file I forgot in the last commit. (diff) | |
download | opensim-SC-2940f325436f5b07dcfbac03545b3eb96e20ffcf.zip opensim-SC-2940f325436f5b07dcfbac03545b3eb96e20ffcf.tar.gz opensim-SC-2940f325436f5b07dcfbac03545b3eb96e20ffcf.tar.bz2 opensim-SC-2940f325436f5b07dcfbac03545b3eb96e20ffcf.tar.xz |
Warp3DCachedImageModule from Christopher Latza.
From -
https://clatza.dev/OpenSim/OpenSim.Modules.Warp3DCachedImageModule.git
Commit ad0aa59f53ae77c85a7c745d9af5aa70187568ba on 17/5/20 1:37 AM
Diffstat (limited to 'addon-modules/OpenSim.Modules.Warp3DCachedImageModule/src/TerrainSplat.cs')
-rw-r--r-- | addon-modules/OpenSim.Modules.Warp3DCachedImageModule/src/TerrainSplat.cs | 470 |
1 files changed, 470 insertions, 0 deletions
diff --git a/addon-modules/OpenSim.Modules.Warp3DCachedImageModule/src/TerrainSplat.cs b/addon-modules/OpenSim.Modules.Warp3DCachedImageModule/src/TerrainSplat.cs new file mode 100644 index 0000000..f60beaf --- /dev/null +++ b/addon-modules/OpenSim.Modules.Warp3DCachedImageModule/src/TerrainSplat.cs | |||
@@ -0,0 +1,470 @@ | |||
1 | /* | ||
2 | * Copyright (c) Contributors, http://opensimulator.org/ | ||
3 | * See CONTRIBUTORS.TXT for a full list of copyright holders. | ||
4 | * | ||
5 | * Redistribution and use in source and binary forms, with or without | ||
6 | * modification, are permitted provided that the following conditions are met: | ||
7 | * * Redistributions of source code must retain the above copyright | ||
8 | * notice, this list of conditions and the following disclaimer. | ||
9 | * * Redistributions in binary form must reproduce the above copyright | ||
10 | * notice, this list of conditions and the following disclaimer in the | ||
11 | * documentation and/or other materials provided with the distribution. | ||
12 | * * Neither the name of the OpenSimulator Project nor the | ||
13 | * names of its contributors may be used to endorse or promote products | ||
14 | * derived from this software without specific prior written permission. | ||
15 | * | ||
16 | * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY | ||
17 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
19 | * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY | ||
20 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||
23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
26 | */ | ||
27 | |||
28 | using System; | ||
29 | using System.Diagnostics; | ||
30 | using System.Drawing; | ||
31 | using System.Drawing.Imaging; | ||
32 | using log4net; | ||
33 | using OpenMetaverse; | ||
34 | using OpenSim.Framework; | ||
35 | using OpenSim.Region.Framework.Interfaces; | ||
36 | using OpenSim.Services.Interfaces; | ||
37 | |||
38 | namespace OpenSim.Region.CoreModules.World.Warp3DMap | ||
39 | { | ||
40 | public static class TerrainSplat | ||
41 | { | ||
42 | #region Constants | ||
43 | |||
44 | private static readonly UUID DIRT_DETAIL = new UUID("0bc58228-74a0-7e83-89bc-5c23464bcec5"); | ||
45 | private static readonly UUID GRASS_DETAIL = new UUID("63338ede-0037-c4fd-855b-015d77112fc8"); | ||
46 | private static readonly UUID MOUNTAIN_DETAIL = new UUID("303cd381-8560-7579-23f1-f0a880799740"); | ||
47 | private static readonly UUID ROCK_DETAIL = new UUID("53a2f406-4895-1d13-d541-d2e3b86bc19c"); | ||
48 | |||
49 | private static readonly UUID[] DEFAULT_TERRAIN_DETAIL = new UUID[] | ||
50 | { | ||
51 | DIRT_DETAIL, | ||
52 | GRASS_DETAIL, | ||
53 | MOUNTAIN_DETAIL, | ||
54 | ROCK_DETAIL | ||
55 | }; | ||
56 | |||
57 | private static readonly Color[] DEFAULT_TERRAIN_COLOR = new Color[] | ||
58 | { | ||
59 | Color.FromArgb(255, 164, 136, 117), | ||
60 | Color.FromArgb(255, 65, 87, 47), | ||
61 | Color.FromArgb(255, 157, 145, 131), | ||
62 | Color.FromArgb(255, 125, 128, 130) | ||
63 | }; | ||
64 | |||
65 | private static readonly UUID TERRAIN_CACHE_MAGIC = new UUID("2c0c7ef2-56be-4eb8-aacb-76712c535b4b"); | ||
66 | |||
67 | #endregion Constants | ||
68 | |||
69 | private static readonly ILog m_log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name); | ||
70 | private static string LogHeader = "[WARP3D TERRAIN SPLAT]"; | ||
71 | |||
72 | /// <summary> | ||
73 | /// Builds a composited terrain texture given the region texture | ||
74 | /// and heightmap settings | ||
75 | /// </summary> | ||
76 | /// <param name="terrain">Terrain heightmap</param> | ||
77 | /// <param name="regionInfo">Region information including terrain texture parameters</param> | ||
78 | /// <returns>A 256x256 square RGB texture ready for rendering</returns> | ||
79 | /// <remarks>Based on the algorithm described at http://opensimulator.org/wiki/Terrain_Splatting | ||
80 | /// Note we create a 256x256 dimension texture even if the actual terrain is larger. | ||
81 | /// </remarks> | ||
82 | |||
83 | public static Bitmap Splat(ITerrainChannel terrain, UUID[] textureIDs, | ||
84 | float[] startHeights, float[] heightRanges, | ||
85 | uint regionPositionX, uint regionPositionY, | ||
86 | IAssetService assetService, IJ2KDecoder decoder, | ||
87 | bool textureTerrain, bool averagetextureTerrain, | ||
88 | int twidth, int theight) | ||
89 | { | ||
90 | Debug.Assert(textureIDs.Length == 4); | ||
91 | Debug.Assert(startHeights.Length == 4); | ||
92 | Debug.Assert(heightRanges.Length == 4); | ||
93 | |||
94 | Bitmap[] detailTexture = new Bitmap[4]; | ||
95 | |||
96 | byte[] mapColorsRed = new byte[4]; | ||
97 | byte[] mapColorsGreen = new byte[4]; | ||
98 | byte[] mapColorsBlue = new byte[4]; | ||
99 | |||
100 | bool usecolors = false; | ||
101 | |||
102 | if (textureTerrain) | ||
103 | { | ||
104 | // Swap empty terrain textureIDs with default IDs | ||
105 | for(int i = 0; i < textureIDs.Length; i++) | ||
106 | { | ||
107 | if(textureIDs[i] == UUID.Zero) | ||
108 | textureIDs[i] = DEFAULT_TERRAIN_DETAIL[i]; | ||
109 | } | ||
110 | |||
111 | #region Texture Fetching | ||
112 | |||
113 | if(assetService != null) | ||
114 | { | ||
115 | for(int i = 0; i < 4; i++) | ||
116 | { | ||
117 | AssetBase asset = null; | ||
118 | |||
119 | // asset cache indexes are strings | ||
120 | string cacheName ="MAP-Patch" + textureIDs[i].ToString(); | ||
121 | |||
122 | // Try to fetch a cached copy of the decoded/resized version of this texture | ||
123 | asset = assetService.GetCached(cacheName); | ||
124 | if(asset != null) | ||
125 | { | ||
126 | try | ||
127 | { | ||
128 | using(System.IO.MemoryStream stream = new System.IO.MemoryStream(asset.Data)) | ||
129 | detailTexture[i] = (Bitmap)Image.FromStream(stream); | ||
130 | |||
131 | if(detailTexture[i].PixelFormat != PixelFormat.Format24bppRgb || | ||
132 | detailTexture[i].Width != 16 || detailTexture[i].Height != 16) | ||
133 | { | ||
134 | detailTexture[i].Dispose(); | ||
135 | detailTexture[i] = null; | ||
136 | } | ||
137 | } | ||
138 | catch(Exception ex) | ||
139 | { | ||
140 | m_log.Warn("Failed to decode cached terrain patch texture" + textureIDs[i] + "): " + ex.Message); | ||
141 | } | ||
142 | } | ||
143 | |||
144 | if(detailTexture[i] == null) | ||
145 | { | ||
146 | // Try to fetch the original JPEG2000 texture, resize if needed, and cache as PNG | ||
147 | asset = assetService.Get(textureIDs[i].ToString()); | ||
148 | if(asset != null) | ||
149 | { | ||
150 | try | ||
151 | { | ||
152 | detailTexture[i] = (Bitmap)decoder.DecodeToImage(asset.Data); | ||
153 | } | ||
154 | catch(Exception ex) | ||
155 | { | ||
156 | m_log.Warn("Failed to decode terrain texture " + asset.ID + ": " + ex.Message); | ||
157 | } | ||
158 | } | ||
159 | |||
160 | if(detailTexture[i] != null) | ||
161 | { | ||
162 | if(detailTexture[i].PixelFormat != PixelFormat.Format24bppRgb || | ||
163 | detailTexture[i].Width != 16 || detailTexture[i].Height != 16) | ||
164 | using(Bitmap origBitmap = detailTexture[i]) | ||
165 | detailTexture[i] = Util.ResizeImageSolid(origBitmap, 16, 16); | ||
166 | |||
167 | // Save the decoded and resized texture to the cache | ||
168 | byte[] data; | ||
169 | using(System.IO.MemoryStream stream = new System.IO.MemoryStream()) | ||
170 | { | ||
171 | detailTexture[i].Save(stream, ImageFormat.Png); | ||
172 | data = stream.ToArray(); | ||
173 | } | ||
174 | |||
175 | // Cache a PNG copy of this terrain texture | ||
176 | AssetBase newAsset = new AssetBase | ||
177 | { | ||
178 | Data = data, | ||
179 | Description = "PNG", | ||
180 | Flags = AssetFlags.Collectable, | ||
181 | FullID = UUID.Zero, | ||
182 | ID = cacheName, | ||
183 | Local = true, | ||
184 | Name = String.Empty, | ||
185 | Temporary = true, | ||
186 | Type = (sbyte)AssetType.Unknown | ||
187 | }; | ||
188 | newAsset.Metadata.ContentType = "image/png"; | ||
189 | assetService.Store(newAsset); | ||
190 | } | ||
191 | } | ||
192 | } | ||
193 | } | ||
194 | |||
195 | #endregion Texture Fetching | ||
196 | if(averagetextureTerrain) | ||
197 | { | ||
198 | for(int t = 0; t < 4; t++) | ||
199 | { | ||
200 | usecolors = true; | ||
201 | if(detailTexture[t] == null) | ||
202 | { | ||
203 | mapColorsRed[t] = DEFAULT_TERRAIN_COLOR[t].R; | ||
204 | mapColorsGreen[t] = DEFAULT_TERRAIN_COLOR[t].G; | ||
205 | mapColorsBlue[t] = DEFAULT_TERRAIN_COLOR[t].B; | ||
206 | continue; | ||
207 | } | ||
208 | |||
209 | int npixeis = 0; | ||
210 | int cR = 0; | ||
211 | int cG = 0; | ||
212 | int cB = 0; | ||
213 | |||
214 | BitmapData bmdata = detailTexture[t].LockBits(new Rectangle(0, 0, 16, 16), | ||
215 | ImageLockMode.ReadOnly, detailTexture[t].PixelFormat); | ||
216 | |||
217 | npixeis = bmdata.Height * bmdata.Width; | ||
218 | int ylen = bmdata.Height * bmdata.Stride; | ||
219 | |||
220 | unsafe | ||
221 | { | ||
222 | for(int y = 0; y < ylen; y += bmdata.Stride) | ||
223 | { | ||
224 | byte* ptrc = (byte*)bmdata.Scan0 + y; | ||
225 | for(int x = 0 ; x < bmdata.Width; ++x) | ||
226 | { | ||
227 | cR += *(ptrc++); | ||
228 | cG += *(ptrc++); | ||
229 | cB += *(ptrc++); | ||
230 | } | ||
231 | } | ||
232 | |||
233 | } | ||
234 | detailTexture[t].UnlockBits(bmdata); | ||
235 | detailTexture[t].Dispose(); | ||
236 | |||
237 | mapColorsRed[t] = (byte)Util.Clamp(cR / npixeis, 0 , 255); | ||
238 | mapColorsGreen[t] = (byte)Util.Clamp(cG / npixeis, 0 , 255); | ||
239 | mapColorsBlue[t] = (byte)Util.Clamp(cB / npixeis, 0 , 255); | ||
240 | } | ||
241 | } | ||
242 | else | ||
243 | { | ||
244 | // Fill in any missing textures with a solid color | ||
245 | for(int i = 0; i < 4; i++) | ||
246 | { | ||
247 | if(detailTexture[i] == null) | ||
248 | { | ||
249 | m_log.DebugFormat("{0} Missing terrain texture for layer {1}. Filling with solid default color", LogHeader, i); | ||
250 | |||
251 | // Create a solid color texture for this layer | ||
252 | detailTexture[i] = new Bitmap(16, 16, PixelFormat.Format24bppRgb); | ||
253 | using(Graphics gfx = Graphics.FromImage(detailTexture[i])) | ||
254 | { | ||
255 | using(SolidBrush brush = new SolidBrush(DEFAULT_TERRAIN_COLOR[i])) | ||
256 | gfx.FillRectangle(brush, 0, 0, 16, 16); | ||
257 | } | ||
258 | } | ||
259 | else | ||
260 | { | ||
261 | if(detailTexture[i].Width != 16 || detailTexture[i].Height != 16) | ||
262 | { | ||
263 | using(Bitmap origBitmap = detailTexture[i]) | ||
264 | detailTexture[i] = Util.ResizeImageSolid(origBitmap, 16, 16); | ||
265 | } | ||
266 | } | ||
267 | } | ||
268 | } | ||
269 | } | ||
270 | else | ||
271 | { | ||
272 | usecolors = true; | ||
273 | for(int t = 0; t < 4; t++) | ||
274 | { | ||
275 | mapColorsRed[t] = DEFAULT_TERRAIN_COLOR[t].R; | ||
276 | mapColorsGreen[t] = DEFAULT_TERRAIN_COLOR[t].G; | ||
277 | mapColorsBlue[t] = DEFAULT_TERRAIN_COLOR[t].B; | ||
278 | } | ||
279 | } | ||
280 | |||
281 | #region Layer Map | ||
282 | |||
283 | float xFactor = terrain.Width / twidth; | ||
284 | float yFactor = terrain.Height / theight; | ||
285 | |||
286 | #endregion Layer Map | ||
287 | |||
288 | #region Texture Compositing | ||
289 | |||
290 | Bitmap output = new Bitmap(twidth, theight, PixelFormat.Format24bppRgb); | ||
291 | BitmapData outputData = output.LockBits(new Rectangle(0, 0, twidth, theight), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb); | ||
292 | |||
293 | // Unsafe work as we lock down the source textures for quicker access and access the | ||
294 | // pixel data directly | ||
295 | float invtwitdthMinus1 = 1.0f / (twidth - 1); | ||
296 | float invtheightMinus1 = 1.0f / (theight - 1); | ||
297 | int ty; | ||
298 | int tx; | ||
299 | float pctx; | ||
300 | float pcty; | ||
301 | float height; | ||
302 | float layer; | ||
303 | float layerDiff; | ||
304 | int l0; | ||
305 | int l1; | ||
306 | uint yglobalpos; | ||
307 | |||
308 | if(usecolors) | ||
309 | { | ||
310 | float a; | ||
311 | float b; | ||
312 | unsafe | ||
313 | { | ||
314 | byte* ptrO; | ||
315 | for(int y = 0; y < theight; ++y) | ||
316 | { | ||
317 | pcty = y * invtheightMinus1; | ||
318 | ptrO = (byte*)outputData.Scan0 + y * outputData.Stride; | ||
319 | ty = (int)(y * yFactor); | ||
320 | yglobalpos = (uint)ty + regionPositionY; | ||
321 | |||
322 | for(int x = 0; x < twidth; ++x) | ||
323 | { | ||
324 | tx = (int)(x * xFactor); | ||
325 | pctx = x * invtwitdthMinus1; | ||
326 | height = (float)terrain[tx, ty]; | ||
327 | layer = getLayerTex(height, pctx, pcty, | ||
328 | (uint)tx + regionPositionX, yglobalpos, | ||
329 | startHeights, heightRanges); | ||
330 | |||
331 | // Select two textures | ||
332 | l0 = (int)layer; | ||
333 | l1 = Math.Min(l0 + 1, 3); | ||
334 | |||
335 | layerDiff = layer - l0; | ||
336 | |||
337 | a = mapColorsRed[l0]; | ||
338 | b = mapColorsRed[l1]; | ||
339 | *(ptrO++) = (byte)(a + layerDiff * (b - a)); | ||
340 | |||
341 | a = mapColorsGreen[l0]; | ||
342 | b = mapColorsGreen[l1]; | ||
343 | *(ptrO++) = (byte)(a + layerDiff * (b - a)); | ||
344 | |||
345 | a = mapColorsBlue[l0]; | ||
346 | b = mapColorsBlue[l1]; | ||
347 | *(ptrO++) = (byte)(a + layerDiff * (b - a)); | ||
348 | } | ||
349 | } | ||
350 | } | ||
351 | } | ||
352 | else | ||
353 | { | ||
354 | float aB; | ||
355 | float aG; | ||
356 | float aR; | ||
357 | float bB; | ||
358 | float bG; | ||
359 | float bR; | ||
360 | |||
361 | unsafe | ||
362 | { | ||
363 | // Get handles to all of the texture data arrays | ||
364 | BitmapData[] datas = new BitmapData[] | ||
365 | { | ||
366 | detailTexture[0].LockBits(new Rectangle(0, 0, 16, 16), ImageLockMode.ReadOnly, detailTexture[0].PixelFormat), | ||
367 | detailTexture[1].LockBits(new Rectangle(0, 0, 16, 16), ImageLockMode.ReadOnly, detailTexture[1].PixelFormat), | ||
368 | detailTexture[2].LockBits(new Rectangle(0, 0, 16, 16), ImageLockMode.ReadOnly, detailTexture[2].PixelFormat), | ||
369 | detailTexture[3].LockBits(new Rectangle(0, 0, 16, 16), ImageLockMode.ReadOnly, detailTexture[3].PixelFormat) | ||
370 | }; | ||
371 | |||
372 | byte* ptr; | ||
373 | byte* ptrO; | ||
374 | for(int y = 0; y < theight; y++) | ||
375 | { | ||
376 | pcty = y * invtheightMinus1; | ||
377 | int ypatch = ((int)(y * yFactor) & 0x0f) * datas[0].Stride; | ||
378 | ptrO = (byte*)outputData.Scan0 + y * outputData.Stride; | ||
379 | ty = (int)(y * yFactor); | ||
380 | yglobalpos = (uint)ty + regionPositionY; | ||
381 | |||
382 | for(int x = 0; x < twidth; x++) | ||
383 | { | ||
384 | tx = (int)(x * xFactor); | ||
385 | pctx = x * invtwitdthMinus1; | ||
386 | height = (float)terrain[tx, ty]; | ||
387 | layer = getLayerTex(height, pctx, pcty, | ||
388 | (uint)tx + regionPositionX, yglobalpos, | ||
389 | startHeights, heightRanges); | ||
390 | |||
391 | // Select two textures | ||
392 | l0 = (int)layer; | ||
393 | layerDiff = layer - l0; | ||
394 | |||
395 | int patchOffset = (tx & 0x0f) * 3 + ypatch; | ||
396 | |||
397 | ptr = (byte*)datas[l0].Scan0 + patchOffset; | ||
398 | aB = *(ptr++); | ||
399 | aG = *(ptr++); | ||
400 | aR = *(ptr); | ||
401 | |||
402 | l1 = Math.Min(l0 + 1, 3); | ||
403 | ptr = (byte*)datas[l1].Scan0 + patchOffset; | ||
404 | bB = *(ptr++); | ||
405 | bG = *(ptr++); | ||
406 | bR = *(ptr); | ||
407 | |||
408 | |||
409 | // Interpolate between the two selected textures | ||
410 | *(ptrO++) = (byte)(aB + layerDiff * (bB - aB)); | ||
411 | *(ptrO++) = (byte)(aG + layerDiff * (bG - aG)); | ||
412 | *(ptrO++) = (byte)(aR + layerDiff * (bR - aR)); | ||
413 | } | ||
414 | } | ||
415 | |||
416 | for(int i = 0; i < detailTexture.Length; i++) | ||
417 | detailTexture[i].UnlockBits(datas[i]); | ||
418 | } | ||
419 | |||
420 | for(int i = 0; i < detailTexture.Length; i++) | ||
421 | if(detailTexture[i] != null) | ||
422 | detailTexture[i].Dispose(); | ||
423 | } | ||
424 | |||
425 | output.UnlockBits(outputData); | ||
426 | |||
427 | //output.Save("terr.png",ImageFormat.Png); | ||
428 | |||
429 | #endregion Texture Compositing | ||
430 | |||
431 | return output; | ||
432 | } | ||
433 | |||
434 | [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] | ||
435 | private static float getLayerTex(float height, float pctX, float pctY, uint X, uint Y, | ||
436 | float[] startHeights, float[] heightRanges) | ||
437 | { | ||
438 | // Use bilinear interpolation between the four corners of start height and | ||
439 | // height range to select the current values at this position | ||
440 | float startHeight = ImageUtils.Bilinear( | ||
441 | startHeights[0], startHeights[2], | ||
442 | startHeights[1], startHeights[3], | ||
443 | pctX, pctY); | ||
444 | if (float.IsNaN(startHeight)) | ||
445 | return 0; | ||
446 | |||
447 | startHeight = Utils.Clamp(startHeight, 0f, 255f); | ||
448 | |||
449 | float heightRange = ImageUtils.Bilinear( | ||
450 | heightRanges[0], heightRanges[2], | ||
451 | heightRanges[1], heightRanges[3], | ||
452 | pctX, pctY); | ||
453 | heightRange = Utils.Clamp(heightRange, 0f, 255f); | ||
454 | if(heightRange == 0f || float.IsNaN(heightRange)) | ||
455 | return 0; | ||
456 | |||
457 | // Generate two frequencies of perlin noise based on our global position | ||
458 | // The magic values were taken from http://opensimulator.org/wiki/Terrain_Splatting | ||
459 | float sX = X * 0.20319f; | ||
460 | float sY = Y * 0.20319f; | ||
461 | |||
462 | float noise = Perlin.noise2(sX * 0.222222f, sY * 0.222222f) * 13.0f; | ||
463 | noise += Perlin.turbulence2(sX, sY, 2f) * 4.5f; | ||
464 | |||
465 | // Combine the current height, generated noise, start height, and height range parameters, then scale all of it | ||
466 | float layer = ((height + noise - startHeight) / heightRange) * 4f; | ||
467 | return Utils.Clamp(layer, 0f, 3f); | ||
468 | } | ||
469 | } | ||
470 | } | ||