From 1d2a6bb16b97aed658f8c0893128f4811bb878ae Mon Sep 17 00:00:00 2001 From: neki-dev Date: Fri, 12 Jul 2024 12:51:25 +0300 Subject: [PATCH] Updated Perlin util --- dist/index.js | 2 +- dist/utils/perlin/const.d.ts | 6 + dist/utils/perlin/index.d.ts | 9 +- dist/utils/seed/index.d.ts | 4 +- package.json | 2 +- src/generator/index.ts | 8 +- src/utils/perlin/const.ts | 9 ++ src/utils/perlin/index.ts | 219 +++++++++++++++++++---------------- src/utils/seed/index.ts | 18 +-- src/world/index.ts | 4 - 10 files changed, 158 insertions(+), 123 deletions(-) create mode 100644 dist/utils/perlin/const.d.ts create mode 100644 src/utils/perlin/const.ts diff --git a/dist/index.js b/dist/index.js index d907d59..a0fa8e3 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1 +1 @@ -(()=>{"use strict";var e={997:(e,t,o)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.WorldGenerator=void 0;const r=o(871),i=o(9),n=o(128),l=o(6);t.WorldGenerator=class{constructor(e){this.biomes=[],this.config=e}addBiome(e,t){const o=new l.WorldBiome(e,t);return this.biomes.push(o),o}clearBiomes(){this.biomes=[]}getBiomes(){return this.biomes}peakBiome(e){var t;return null!==(t=this.getBiomes().find((t=>e>=t.lowerBound&&e<=t.upperBound)))&&void 0!==t?t:null}generate(e){var t,o,l;const s=null!==(t=null==e?void 0:e.seed)&&void 0!==t?t:(0,i.generateSeed)(null==e?void 0:e.seedSize),u=[];for(let t=0;t{Object.defineProperty(t,"__esModule",{value:!0})},465:function(e,t,o){var r=this&&this.__createBinding||(Object.create?function(e,t,o,r){void 0===r&&(r=o);var i=Object.getOwnPropertyDescriptor(t,o);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[o]}}),Object.defineProperty(e,r,i)}:function(e,t,o,r){void 0===r&&(r=o),e[r]=t[o]}),i=this&&this.__exportStar||function(e,t){for(var o in e)"default"===o||Object.prototype.hasOwnProperty.call(t,o)||r(t,e,o)};Object.defineProperty(t,"__esModule",{value:!0}),i(o(997),t),i(o(147),t),i(o(128),t),i(o(383),t),i(o(6),t),i(o(346),t),i(o(871),t),i(o(9),t)},871:(e,t)=>{function o(e,t,o=[0,1]){return Math.max(o[0],Math.min(o[1],null!=e?e:t))}function r(e){return.5*(1-Math.cos(e*Math.PI))}function i(e,t,o){const r=t/2,i=Math.abs(r-e),n=r*(1-o);if(i=1&&(w++,B--),y<<=1,O*=2,O>=1&&(y++,O--)}return f&&(P>.5?P=Math.pow(P,(1.5-P)/1.1):P<.5&&(P=Math.pow(P,1.1*(1.5-P)))),P=Math.pow(P,h),c&&(P*=i(n,u.width,c)*i(l,u.height,c)),P}},9:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.generateSeed=void 0,t.generateSeed=function(e=512){const t=[];for(let o=0;o{Object.defineProperty(t,"__esModule",{value:!0}),t.WorldBiome=void 0,t.WorldBiome=class{constructor(e,t){var o,r;this.lowerBound=Math.max(0,null!==(o=e.lowerBound)&&void 0!==o?o:0),this.upperBound=Math.min(1,null!==(r=e.upperBound)&&void 0!==r?r:1),this.data=t}}},346:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0})},128:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.World=void 0,t.World=class{constructor(e,t){this.matrix=[],this.width=e[0].length,this.height=e.length,this.matrix=e,this.seed=t}getMatrix(){return this.matrix}each(e){for(let t=0;t=this.height||e.x>=this.width)throw Error(`Position [${e.x},${e.y}] is out of world bounds`);this.matrix[e.y][e.x]=t}}},383:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0})}},t={},o=function o(r){var i=t[r];if(void 0!==i)return i.exports;var n=t[r]={exports:{}};return e[r].call(n.exports,n,n.exports,o),n.exports}(465);module.exports=o})(); \ No newline at end of file +(()=>{"use strict";var e={997:(e,t,o)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.WorldGenerator=void 0;const i=o(871),r=o(9),n=o(128),s=o(6);t.WorldGenerator=class{constructor(e){this.biomes=[],this.config=e}addBiome(e,t){const o=new s.WorldBiome(e,t);return this.biomes.push(o),o}clearBiomes(){this.biomes=[]}getBiomes(){return this.biomes}peakBiome(e){var t;return null!==(t=this.getBiomes().find((t=>e>=t.lowerBound&&e<=t.upperBound)))&&void 0!==t?t:null}generate(e){var t,o,s;const l=null!==(t=null==e?void 0:e.seed)&&void 0!==t?t:r.Seed.generate(null==e?void 0:e.seedSize),a=[];for(let t=0;t{Object.defineProperty(t,"__esModule",{value:!0})},465:function(e,t,o){var i=this&&this.__createBinding||(Object.create?function(e,t,o,i){void 0===i&&(i=o);var r=Object.getOwnPropertyDescriptor(t,o);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[o]}}),Object.defineProperty(e,i,r)}:function(e,t,o,i){void 0===i&&(i=o),e[i]=t[o]}),r=this&&this.__exportStar||function(e,t){for(var o in e)"default"===o||Object.prototype.hasOwnProperty.call(t,o)||i(t,e,o)};Object.defineProperty(t,"__esModule",{value:!0}),r(o(997),t),r(o(147),t),r(o(128),t),r(o(383),t),r(o(6),t),r(o(346),t),r(o(871),t),r(o(9),t)},708:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.PERLIN_AVG_POWER=t.PERLIN_AMP_FALLOFF=t.PERLIN_ZWRAP=t.PERLIN_ZWRAPB=t.PERLIN_YWRAP=t.PERLIN_YWRAPB=void 0,t.PERLIN_YWRAPB=4,t.PERLIN_YWRAP=1<{Object.defineProperty(t,"__esModule",{value:!0}),t.Perlin=void 0;const i=o(708);t.Perlin=class{static generate({x:e,y:t,seed:o,config:r}){const{frequencyChange:n,borderSmoothness:s,heightAveraging:l,heightRedistribution:a,falloff:h}=this.normalizeConfig(r),d=o.length-1,u=e/r.width*n,c=t/r.height*n;let f=Math.floor(u),P=Math.floor(c),_=u-f,g=c-P,v=0,p=.5;for(let e=0;e=1&&(f++,_--),P<<=1,g*=2,g>=1&&(P++,g--)}return l&&(v>.5?v=Math.pow(v,(1.5-v)/i.PERLIN_AVG_POWER):v<.5&&(v=Math.pow(v,(1.5-v)*i.PERLIN_AVG_POWER))),v=Math.pow(v,a),h>0&&(v*=this.heightFalloff(e,r.width,h)*this.heightFalloff(t,r.height,h)),v}static clamp(e,t,o=[0,1]){return Math.max(o[0],Math.min(o[1],null!=e?e:t))}static scaledCosine(e){return.5*(1-Math.cos(e*Math.PI))}static smootherStep(e){return 3*Math.pow(e,2)-2*Math.pow(e,3)}static heightFalloff(e,t,o){const i=t/2,r=Math.abs(i-e),n=i*(1-o);if(r{Object.defineProperty(t,"__esModule",{value:!0}),t.Seed=void 0,t.Seed=class{static generate(e=512){const t=[];for(let o=0;o{Object.defineProperty(t,"__esModule",{value:!0}),t.WorldBiome=void 0,t.WorldBiome=class{constructor(e,t){var o,i;this.lowerBound=Math.max(0,null!==(o=e.lowerBound)&&void 0!==o?o:0),this.upperBound=Math.min(1,null!==(i=e.upperBound)&&void 0!==i?i:1),this.data=t}}},346:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0})},128:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.World=void 0,t.World=class{constructor(e,t){this.matrix=[],this.width=e[0].length,this.height=e.length,this.matrix=e,this.seed=t}getMatrix(){return this.matrix}each(e){for(let t=0;t{Object.defineProperty(t,"__esModule",{value:!0})}},t={},o=function o(i){var r=t[i];if(void 0!==r)return r.exports;var n=t[i]={exports:{}};return e[i].call(n.exports,n,n.exports,o),n.exports}(465);module.exports=o})(); \ No newline at end of file diff --git a/dist/utils/perlin/const.d.ts b/dist/utils/perlin/const.d.ts new file mode 100644 index 0000000..94428d2 --- /dev/null +++ b/dist/utils/perlin/const.d.ts @@ -0,0 +1,6 @@ +export declare const PERLIN_YWRAPB = 4; +export declare const PERLIN_YWRAP: number; +export declare const PERLIN_ZWRAPB = 8; +export declare const PERLIN_ZWRAP: number; +export declare const PERLIN_AMP_FALLOFF = 0.5; +export declare const PERLIN_AVG_POWER = 1.1; diff --git a/dist/utils/perlin/index.d.ts b/dist/utils/perlin/index.d.ts index 61c5190..3000cdb 100644 --- a/dist/utils/perlin/index.d.ts +++ b/dist/utils/perlin/index.d.ts @@ -1,2 +1,9 @@ import type { PerlinParameters } from "./types"; -export declare function generateNoise(parameters: PerlinParameters): number; +export declare class Perlin { + static generate({ x, y, seed, config }: PerlinParameters): number; + private static clamp; + private static scaledCosine; + private static smootherStep; + private static heightFalloff; + private static normalizeConfig; +} diff --git a/dist/utils/seed/index.d.ts b/dist/utils/seed/index.d.ts index fbb9a33..4f59f1e 100644 --- a/dist/utils/seed/index.d.ts +++ b/dist/utils/seed/index.d.ts @@ -1 +1,3 @@ -export declare function generateSeed(size?: number): number[]; +export declare class Seed { + static generate(size?: number): number[]; +} diff --git a/package.json b/package.json index dd2d34e..e4bdbdc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gen-biome", "description": "Procedural generation of 2D maps with distinct biomes", - "version": "3.0.1", + "version": "3.0.2", "keywords": [ "map", "generation", diff --git a/src/generator/index.ts b/src/generator/index.ts index 15ac630..8a18477 100644 --- a/src/generator/index.ts +++ b/src/generator/index.ts @@ -1,6 +1,6 @@ import type { WorldGenerationParams } from "./types"; -import { generateNoise } from "../utils/perlin"; -import { generateSeed } from "../utils/seed"; +import { Perlin } from "../utils/perlin"; +import { Seed } from "../utils/seed"; import { World } from "../world"; import { WorldBiome } from "../world/biome"; import type { WorldConfig } from "../world/types"; @@ -37,13 +37,13 @@ export class WorldGenerator { } public generate(params?: WorldGenerationParams): World { - const currentSeed = params?.seed ?? generateSeed(params?.seedSize); + const currentSeed = params?.seed ?? Seed.generate(params?.seedSize); const matrix: T[][] = []; for (let y = 0; y < this.config.height; y++) { matrix[y] = []; for (let x = 0; x < this.config.width; x++) { - const height = generateNoise({ + const height = Perlin.generate({ config: this.config, seed: currentSeed, x: x + (params?.offsetX ?? 0), diff --git a/src/utils/perlin/const.ts b/src/utils/perlin/const.ts new file mode 100644 index 0000000..5bb1f47 --- /dev/null +++ b/src/utils/perlin/const.ts @@ -0,0 +1,9 @@ +export const PERLIN_YWRAPB = 4; +export const PERLIN_YWRAP = 1 << PERLIN_YWRAPB; + +export const PERLIN_ZWRAPB = 8; +export const PERLIN_ZWRAP = 1 << PERLIN_ZWRAPB; + +export const PERLIN_AMP_FALLOFF = 0.5; + +export const PERLIN_AVG_POWER = 1.1; diff --git a/src/utils/perlin/index.ts b/src/utils/perlin/index.ts index f0b9250..6662658 100644 --- a/src/utils/perlin/index.ts +++ b/src/utils/perlin/index.ts @@ -1,121 +1,134 @@ +import type { WorldConfig } from "../../world/types"; +import { + PERLIN_AMP_FALLOFF, + PERLIN_AVG_POWER, + PERLIN_YWRAP, + PERLIN_YWRAPB, + PERLIN_ZWRAP, +} from "./const"; import type { PerlinParameters } from "./types"; -function clamp( - value: number | undefined, - defaultValue: number, - limit: [number, number] = [0, 1], -) { - return Math.max(limit[0], Math.min(limit[1], value ?? defaultValue)); -} +export class Perlin { + public static generate({ x, y, seed, config }: PerlinParameters): number { + const { + frequencyChange, + borderSmoothness, + heightAveraging, + heightRedistribution, + falloff, + } = this.normalizeConfig(config); + + const size = seed.length - 1; + const cx = (x / config.width) * frequencyChange; + const cy = (y / config.height) * frequencyChange; + + let xi = Math.floor(cx); + let yi = Math.floor(cy); + let xf = cx - xi; + let yf = cy - yi; + + let r = 0; + let ampl = 0.5; + + for (let o = 0; o < borderSmoothness; o++) { + let of = xi + (yi << PERLIN_YWRAPB); + + const rxf = this.scaledCosine(xf); + const ryf = this.scaledCosine(yf); + + let n1 = seed[of & size]; + n1 += rxf * (seed[(of + 1) & size] - n1); + + let n2 = seed[(of + PERLIN_YWRAP) & size]; + n2 += rxf * (seed[(of + PERLIN_YWRAP + 1) & size] - n2); + + n1 += ryf * (n2 - n1); + r += n1 * ampl; + ampl *= PERLIN_AMP_FALLOFF; + of += PERLIN_ZWRAP; + + xi <<= 1; + xf *= 2; + if (xf >= 1.0) { + xi++; + xf--; + } + + yi <<= 1; + yf *= 2; + if (yf >= 1.0) { + yi++; + yf--; + } + } -function scaledCosine(i: number): number { - return 0.5 * (1.0 - Math.cos(i * Math.PI)); -} + if (heightAveraging) { + if (r > 0.5) { + r **= (1.5 - r) / PERLIN_AVG_POWER; + } else if (r < 0.5) { + r **= (1.5 - r) * PERLIN_AVG_POWER; + } + } -function smootherStep(x: number) { - return (3 * x ** 2) - (2 * x ** 3); -} + r **= heightRedistribution; -function heightFalloff(offset: number, length: number, falloff: number) { - const radius = length / 2; - const distance = Math.abs(radius - offset); - const target = radius * (1 - falloff); + if (falloff > 0.0) { + r *= + this.heightFalloff(x, config.width, falloff) * + this.heightFalloff(y, config.height, falloff); + } - if (distance < target) { - return 1; + return r; } - let x = ((distance - target) / radius) / (1 - target / radius); - - x = Math.min(1, Math.max(0, x)); - - return 1 - smootherStep(x); -} + private static clamp( + value: number | undefined, + defaultValue: number, + limit: [number, number] = [0, 1] + ) { + return Math.max(limit[0], Math.min(limit[1], value ?? defaultValue)); + } -export function generateNoise(parameters: PerlinParameters): number { - const { x, y, seed, config } = parameters; - - const frequency = Math.round(clamp(config.frequencyChange, 0.3) * 31 + 1); - const octaves = Math.round((1 - clamp(config.borderSmoothness, 0.5)) * 14 + 1); - const redistribution = 2.0 - clamp(config.heightRedistribution, 1.0, [0.5, 1.5]); - const falloff = clamp(config.falloff, 0.0, [0.0, 0.9]); - const averaging = config.heightAveraging ?? true; - - const PERLIN_SIZE = seed.length - 1; - const PERLIN_YWRAPB = 4; - const PERLIN_YWRAP = 1 << PERLIN_YWRAPB; - const PERLIN_ZWRAPB = 8; - const PERLIN_ZWRAP = 1 << PERLIN_ZWRAPB; - const PERLIN_AMP_FALLOFF = 0.5; - const PERLIN_AVG_POWER = 1.1; - - const cx = (x / config.width) * frequency; - const cy = (y / config.height) * frequency; - - let xi = Math.floor(cx); - let yi = Math.floor(cy); - let xf = cx - xi; - let yf = cy - yi; - let rxf; - let ryf; - - let r = 0; - let ampl = 0.5; - - let n1; - let n2; - let n3; - - for (let o = 0; o < octaves; o++) { - let of = xi + (yi << PERLIN_YWRAPB); - - rxf = scaledCosine(xf); - ryf = scaledCosine(yf); - - n1 = seed[of & PERLIN_SIZE]; - n1 += rxf * (seed[(of + 1) & PERLIN_SIZE] - n1); - n2 = seed[(of + PERLIN_YWRAP) & PERLIN_SIZE]; - n2 += rxf * (seed[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n2); - n1 += ryf * (n2 - n1); - - of += PERLIN_ZWRAP; - n2 = seed[of & PERLIN_SIZE]; - n2 += rxf * (seed[(of + 1) & PERLIN_SIZE] - n2); - n3 = seed[(of + PERLIN_YWRAP) & PERLIN_SIZE]; - n3 += rxf * (seed[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n3); - n2 += ryf * (n3 - n2); - - r += n1 * ampl; - ampl *= PERLIN_AMP_FALLOFF; - - xi <<= 1; - xf *= 2; - if (xf >= 1.0) { - xi++; - xf--; - } + private static scaledCosine(i: number): number { + return 0.5 * (1.0 - Math.cos(i * Math.PI)); + } - yi <<= 1; - yf *= 2; - if (yf >= 1.0) { - yi++; - yf--; - } + private static smootherStep(x: number) { + return 3 * x ** 2 - 2 * x ** 3; } - if (averaging) { - if (r > 0.5) { - r **= (1.5 - r) / PERLIN_AVG_POWER; - } else if (r < 0.5) { - r **= (1.5 - r) * PERLIN_AVG_POWER; + private static heightFalloff( + offset: number, + length: number, + falloff: number + ) { + const radius = length / 2; + const distance = Math.abs(radius - offset); + const target = radius * (1 - falloff); + + if (distance < target) { + return 1; } - } - r **= redistribution; + let x = (distance - target) / radius / (1 - target / radius); - if (falloff) { - r *= heightFalloff(x, config.width, falloff) * heightFalloff(y, config.height, falloff); + x = Math.min(1, Math.max(0, x)); + + return 1 - this.smootherStep(x); } - return r; + private static normalizeConfig(config: WorldConfig) { + return { + frequencyChange: Math.round( + this.clamp(config.frequencyChange, 0.3) * 31 + 1 + ), + borderSmoothness: Math.round( + (1 - this.clamp(config.borderSmoothness, 0.5)) * 14 + 1 + ), + heightRedistribution: + 2.0 - this.clamp(config.heightRedistribution, 1.0, [0.5, 1.5]), + falloff: this.clamp(config.falloff, 0.0, [0.0, 0.9]), + heightAveraging: config.heightAveraging ?? true, + }; + } } diff --git a/src/utils/seed/index.ts b/src/utils/seed/index.ts index 173177b..500984f 100644 --- a/src/utils/seed/index.ts +++ b/src/utils/seed/index.ts @@ -1,9 +1,11 @@ -export function generateSeed(size: number = 512) { - const seed: number[] = []; - - for (let i = 0; i < size; i++) { - seed.push(Math.random()); +export class Seed { + public static generate(size: number = 512) { + const seed: number[] = []; + + for (let i = 0; i < size; i++) { + seed.push(Math.random()); + } + + return seed; } - - return seed; -} +} \ No newline at end of file diff --git a/src/world/index.ts b/src/world/index.ts index 0defa31..f4c93c5 100644 --- a/src/world/index.ts +++ b/src/world/index.ts @@ -36,10 +36,6 @@ export class World { } public replaceAt(point: WorldPoint, data: T): void { - if (point.y >= this.height || point.x >= this.width) { - throw Error(`Position [${point.x},${point.y}] is out of world bounds`); - } - this.matrix[point.y][point.x] = data; } }