mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-26 08:26:51 +00:00
Support 1.18 (experimental) snapshots (#158)
* Half support 1.18-experimental-snapshot-1 * Fetch 1.18 presets and improve rendering of lists * Noise preview with deepslate * Biome preview with deepslate * Generalize canvas logic in one hook * Simplify useCanvas * Use mcschema for 1.18 * Improve noise settings preview controls * Fix build * Update deepslate and improve preview caching * Cleanup, remove old preview code * Couple seed between model and preview * Limit output to improve performance + copy feedback For the vanilla overworld dimension (200K lines), it took 2+ seconds to write the output to the textarea Now capped at 10K chars * Add surface_relative_threshold to decorator preview * Improve fixed list errors
This commit is contained in:
@@ -1,62 +1,39 @@
|
||||
import { stringToColor } from '../Utils'
|
||||
import { NormalNoise } from './noise/NormalNoise'
|
||||
import type { BiomeSource, Climate, NoiseOctaves } from 'deepslate'
|
||||
import { FixedBiome, MultiNoise, NoiseGeneratorSettings, NoiseSampler, NormalNoise, Random } from 'deepslate'
|
||||
import { fetchPreset } from '../DataFetcher'
|
||||
import type { VersionId } from '../Schemas'
|
||||
import { deepClone, deepEqual, square, stringToColor } from '../Utils'
|
||||
|
||||
type BiomeColors =Record<string, number[]>
|
||||
type BiomeColors = Record<string, number[]>
|
||||
type BiomeSourceOptions = {
|
||||
octaves: NoiseOctaves,
|
||||
biomeColors: BiomeColors,
|
||||
offset: [number, number],
|
||||
scale: number,
|
||||
res: number,
|
||||
seed: string,
|
||||
seed: bigint,
|
||||
version: VersionId,
|
||||
}
|
||||
|
||||
const NoiseMaps = ['altitude', 'temperature', 'humidity', 'weirdness']
|
||||
let cacheState: any
|
||||
let biomeSourceCache: BiomeSource
|
||||
let climateSamplerCache: Climate.Sampler
|
||||
|
||||
export function biomeSource(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
switch (state?.type?.replace(/^minecraft:/, '')) {
|
||||
case 'multi_noise': return multiNoise(state, img, options)
|
||||
case 'fixed': return fixed(state, img, options)
|
||||
case 'checkerboard': return checkerboard(state, img, options)
|
||||
}
|
||||
}
|
||||
|
||||
function fixed(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
const data = img.data
|
||||
const color = getBiomeColor(state.biome, options.biomeColors)
|
||||
const row = img.width * 4 / options.res
|
||||
const col = 4 / options.res
|
||||
for (let x = 0; x < 200; x += options.res) {
|
||||
for (let y = 0; y < 200; y += options.res) {
|
||||
const i = y * row + x * col
|
||||
data[i] = color[0]
|
||||
data[i + 1] = color[1]
|
||||
data[i + 2] = color[2]
|
||||
data[i + 3] = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkerboard(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
const biomeColorCache: BiomeColors = {}
|
||||
state.biomes?.forEach((b: string) => {
|
||||
biomeColorCache[b] = getBiomeColor(b, options.biomeColors)
|
||||
})
|
||||
export async function biomeMap(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
const { biomeSource, climateSampler } = await getCached(state, options)
|
||||
|
||||
const data = img.data
|
||||
const ox = -options.offset[0] - 100 + options.res / 2
|
||||
const oy = -options.offset[1] - 100 + options.res / 2
|
||||
const oz = -options.offset[1] - 100 + options.res / 2
|
||||
const row = img.width * 4 / options.res
|
||||
const col = 4 / options.res
|
||||
const shift = (state.scale ?? 2) + 2
|
||||
const numBiomes = state.biomes?.length ?? 0
|
||||
for (let x = 0; x < 200; x += options.res) {
|
||||
for (let y = 0; y < 200; y += options.res) {
|
||||
const i = y * row + x * col
|
||||
const xx = (x + ox) * options.scale
|
||||
const yy = (y + oy) * options.scale
|
||||
const j = (((xx >> shift) + (yy >> shift)) % numBiomes + numBiomes) % numBiomes
|
||||
const b = state.biomes?.[j]
|
||||
const color = biomeColorCache[b] ?? [128, 128, 128]
|
||||
for (let z = 0; z < 200; z += options.res) {
|
||||
const i = z * row + x * col
|
||||
const worldX = (x + ox) * options.scale
|
||||
const worldZ = (z + oz) * options.scale
|
||||
const b = biomeSource.getBiome(worldX, 64, worldZ, climateSampler)
|
||||
const color = getBiomeColor(b, options.biomeColors)
|
||||
data[i] = color[0]
|
||||
data[i + 1] = color[1]
|
||||
data[i + 2] = color[2]
|
||||
@@ -65,61 +42,86 @@ function checkerboard(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
function multiNoise(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
if (state.preset?.replace(/^minecraft:/, '') === 'nether') {
|
||||
state = NetherPreset
|
||||
}
|
||||
export async function getBiome(state: any, x: number, z: number, options: BiomeSourceOptions): Promise<string | undefined> {
|
||||
const { biomeSource, climateSampler } = await getCached(state, options)
|
||||
|
||||
const noise = NoiseMaps.map((id, i) => {
|
||||
const config = state[`${id}_noise`]
|
||||
return new NormalNoise(options.seed + i, config.firstOctave, config.amplitudes)
|
||||
})
|
||||
|
||||
const biomeColorCache: BiomeColors = {}
|
||||
state.biomes.forEach((b: any) => {
|
||||
biomeColorCache[b.biome] = getBiomeColor(b.biome, options.biomeColors)
|
||||
})
|
||||
|
||||
const data = img.data
|
||||
const ox = -options.offset[0] - 100 + options.res / 2
|
||||
const oy = -options.offset[1] - 100 + options.res / 2
|
||||
const row = img.width * 4 / options.res
|
||||
const col = 4 / options.res
|
||||
for (let x = 0; x < 200; x += options.res) {
|
||||
for (let y = 0; y < 200; y += options.res) {
|
||||
const i = y * row + x * col
|
||||
const xx = (x + ox) * options.scale
|
||||
const yy = (y + oy) * options.scale
|
||||
const b = closestBiome(noise, state.biomes, xx, yy)
|
||||
const color = biomeColorCache[b] ?? [128, 128, 128]
|
||||
data[i] = color[0]
|
||||
data[i + 1] = color[1]
|
||||
data[i + 2] = color[2]
|
||||
data[i + 3] = 255
|
||||
}
|
||||
}
|
||||
const [xx, zz] = toWorld([x, z], options)
|
||||
return biomeSource.getBiome(xx, 64, zz, climateSampler)
|
||||
}
|
||||
|
||||
export function getBiome(state: any, x: number, y: number, options: BiomeSourceOptions): string | undefined {
|
||||
const [xx, yy] = toWorld([x, y], options)
|
||||
async function getCached(state: any, options: BiomeSourceOptions): Promise<{ biomeSource: BiomeSource, climateSampler: Climate.Sampler }> {
|
||||
const newState = [state, options.octaves, `${options.seed}`, options.version]
|
||||
if (!deepEqual(newState, cacheState)) {
|
||||
cacheState = deepClone(newState)
|
||||
|
||||
biomeSourceCache = await getBiomeSource(state, options)
|
||||
|
||||
const settings = NoiseGeneratorSettings.fromJson({ octaves: options.octaves })
|
||||
const noiseSampler = new NoiseSampler(4, 4, 32, biomeSourceCache, settings.noise, options.octaves, options.seed)
|
||||
climateSamplerCache = noiseSampler.getClimate.bind(noiseSampler)
|
||||
}
|
||||
return {
|
||||
biomeSource: biomeSourceCache,
|
||||
climateSampler: climateSamplerCache,
|
||||
}
|
||||
}
|
||||
|
||||
async function getBiomeSource(state: any, options: BiomeSourceOptions): Promise<BiomeSource> {
|
||||
switch (state?.type?.replace(/^minecraft:/, '')) {
|
||||
case 'multi_noise':
|
||||
const noise = NoiseMaps.map((id, i) => {
|
||||
const config = state[`${id}_noise`]
|
||||
return new NormalNoise(options.seed + i, config.firstOctave, config.amplitudes)
|
||||
})
|
||||
return closestBiome(noise, state.biomes, xx, yy)
|
||||
case 'fixed': return state.biome
|
||||
case 'fixed':
|
||||
return new FixedBiome(state.biome as string)
|
||||
|
||||
case 'checkerboard':
|
||||
const shift = (state.scale ?? 2) + 2
|
||||
const numBiomes = state.biomes?.length ?? 0
|
||||
const j = (((xx >> shift) + (yy >> shift)) % numBiomes + numBiomes) % numBiomes
|
||||
return state.biomes?.[j]
|
||||
return {
|
||||
getBiome(x: number, _y: number, z: number) {
|
||||
const i = (((x >> shift) + (z >> shift)) % numBiomes + numBiomes) % numBiomes
|
||||
return (state.biomes?.[i] as string)
|
||||
},
|
||||
}
|
||||
|
||||
case 'multi_noise':
|
||||
switch(state.preset?.replace(/^minecraft:/, '')) {
|
||||
case 'nether':
|
||||
state = options.version === '1.18' ? NetherPreset18 : NetherPreset
|
||||
break
|
||||
case 'overworld':
|
||||
state = options.version === '1.18' ? await OverworldPreset18() : state
|
||||
break
|
||||
}
|
||||
if (options.version === '1.18') {
|
||||
return MultiNoise.fromJson(state)
|
||||
} else {
|
||||
const noise = ['altitude', 'temperature', 'humidity', 'weirdness']
|
||||
.map((id, i) => {
|
||||
const config = state[`${id}_noise`]
|
||||
return new NormalNoise(new Random(options.seed + BigInt(i)), config)
|
||||
})
|
||||
if (!Array.isArray(state.biomes) || state.biomes.length === 0) {
|
||||
return new FixedBiome('unknown')
|
||||
}
|
||||
return {
|
||||
getBiome(x: number, _y: number, z: number): string {
|
||||
const n = noise.map(n => n.sample(x, z, 0))
|
||||
let minDist = Infinity
|
||||
let minBiome = ''
|
||||
for (const { biome, parameters: p } of state.biomes) {
|
||||
const dist = square(p.altitude - n[0]) + square(p.temperature - n[1]) + square(p.humidity - n[2]) + square(p.weirdness - n[3]) + square(p.offset)
|
||||
if (dist < minDist) {
|
||||
minDist = dist
|
||||
minBiome = biome
|
||||
}
|
||||
}
|
||||
return minBiome
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
throw new Error('Unknown biome source')
|
||||
}
|
||||
|
||||
export function getBiomeColor(biome: string, biomeColors: BiomeColors) {
|
||||
function getBiomeColor(biome: string, biomeColors: BiomeColors) {
|
||||
if (!biome) {
|
||||
return [128, 128, 128]
|
||||
}
|
||||
@@ -130,29 +132,10 @@ export function getBiomeColor(biome: string, biomeColors: BiomeColors) {
|
||||
return color
|
||||
}
|
||||
|
||||
function toWorld([x, y]: [number, number], options: BiomeSourceOptions) {
|
||||
function toWorld([x, z]: [number, number], options: BiomeSourceOptions) {
|
||||
const xx = (x - options.offset[0] - 100 + options.res / 2) * options.scale
|
||||
const yy = (y - options.offset[1] - 100 + options.res / 2) * options.scale
|
||||
return [xx, yy]
|
||||
}
|
||||
|
||||
function closestBiome(noise: NormalNoise[], biomes: any[], x: number, y: number): string {
|
||||
if (!Array.isArray(biomes) || biomes.length === 0) return ''
|
||||
const n = noise.map(n => n.getValue(x, y, 0))
|
||||
let minDist = Infinity
|
||||
let minBiome = ''
|
||||
for (const b of biomes) {
|
||||
const dist = fitness(b.parameters, {altitude: n[0], temperature: n[1], humidity: n[2], weirdness: n[3], offset: 0})
|
||||
if (dist < minDist) {
|
||||
minDist = dist
|
||||
minBiome = b.biome
|
||||
}
|
||||
}
|
||||
return minBiome
|
||||
}
|
||||
|
||||
function fitness(a: any, b: any) {
|
||||
return (a.altitude - b.altitude) * (a.altitude - b.altitude) + (a.temperature - b.temperature) * (a.temperature - b.temperature) + (a.humidity - b.humidity) * (a.humidity - b.humidity) + (a.weirdness - b.weirdness) * (a.weirdness - b.weirdness) + (a.offset - b.offset) * (a.offset - b.offset)
|
||||
const zz = (z - options.offset[1] - 100 + options.res / 2) * options.scale
|
||||
return [xx, zz]
|
||||
}
|
||||
|
||||
const VanillaColors: Record<string, [number, number, number]> = {
|
||||
@@ -235,6 +218,21 @@ const VanillaColors: Record<string, [number, number, number]> = {
|
||||
'minecraft:wooded_badlands_plateau': [176,151,101],
|
||||
'minecraft:wooded_hills': [34,85,28],
|
||||
'minecraft:wooded_mountains': [80,112,80],
|
||||
'minecraft:snowy_slopes': [140, 195, 222],
|
||||
'minecraft:lofty_peaks': [196, 168, 193],
|
||||
'minecraft:snowcapped_peaks': [200, 198, 200],
|
||||
'minecraft:stony_peaks': [82, 92, 103],
|
||||
'minecraft:grove': [150, 150, 189],
|
||||
'minecraft:meadow': [169, 197, 80],
|
||||
'minecraft:lush_caves': [112, 255, 79],
|
||||
'minecraft:dripstone_caves': [140, 124, 0],
|
||||
}
|
||||
|
||||
const NetherPreset = {type:'minecraft:multi_noise',seed:0,altitude_noise:{firstOctave:-7,amplitudes:[1,1]},temperature_noise:{firstOctave:-7,amplitudes:[1,1]},humidity_noise:{firstOctave:-7,amplitudes:[1,1]},weirdness_noise:{firstOctave:-7,amplitudes:[1,1]},biomes:[{biome:'minecraft:nether_wastes',parameters:{altitude:0,temperature:0,humidity:0,weirdness:0,offset:0}},{biome:'minecraft:soul_sand_valley',parameters:{altitude:0,temperature:0,humidity:-0.5,weirdness:0,offset:0}},{biome:'minecraft:crimson_forest',parameters:{altitude:0,temperature:0.4,humidity:0,weirdness:0,offset:0}},{biome:'minecraft:warped_forest',parameters:{altitude:0,temperature:0,humidity:0.5,weirdness:0,offset:0.375}},{biome:'minecraft:basalt_deltas',parameters:{altitude:0,temperature:-0.5,humidity:0,weirdness:0,offset:0.175}}]}
|
||||
|
||||
const NetherPreset18 = {type:'minecraft:multi_noise',biomes:[{biome:'minecraft:nether_wastes',parameters:{temperature:0,humidity:0,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0}},{biome:'minecraft:soul_sand_valley',parameters:{temperature:0,humidity:-0.5,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0}},{biome:'minecraft:crimson_forest',parameters:{temperature:0.4,humidity:0,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0}},{biome:'minecraft:warped_forest',parameters:{temperature:0,humidity:0.5,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0.375}},{biome:'minecraft:basalt_deltas',parameters:{temperature:-0.5,humidity:0,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0.175}}]}
|
||||
|
||||
async function OverworldPreset18() {
|
||||
const overworld = await fetchPreset('1.18', 'dimension', 'overworld')
|
||||
return overworld.generator.biome_source
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import seedrandom from 'seedrandom'
|
||||
import { PerlinNoise, Random } from 'deepslate'
|
||||
import type { VersionId } from '../Schemas'
|
||||
import { clamp, stringToColor } from '../Utils'
|
||||
import { PerlinNoise } from './noise/PerlinNoise'
|
||||
|
||||
type BlockPos = [number, number, number]
|
||||
type Placement = [BlockPos, number]
|
||||
@@ -9,10 +8,13 @@ type Placement = [BlockPos, number]
|
||||
type PlacementContext = {
|
||||
placements: Placement[],
|
||||
features: string[],
|
||||
random: seedrandom.prng,
|
||||
random: Random,
|
||||
biomeInfoNoise: PerlinNoise,
|
||||
seaLevel: number,
|
||||
version: VersionId,
|
||||
nextFloat(): number,
|
||||
nextInt(max: number): number,
|
||||
sampleInt(provider: any): number,
|
||||
}
|
||||
|
||||
const terrain = [50, 50, 51, 51, 52, 52, 53, 54, 56, 57, 57, 58, 58, 59, 60, 60, 60, 59, 59, 59, 60, 61, 61, 62, 63, 63, 64, 64, 64, 65, 65, 66, 66, 65, 65, 66, 66, 67, 67, 67, 68, 69, 71, 73, 74, 76, 79, 80, 81, 81, 82, 83, 83, 82, 82, 81, 81, 80, 80, 80, 81, 81, 82, 82]
|
||||
@@ -28,18 +30,21 @@ const featureColors = [
|
||||
|
||||
export type DecoratorOptions = {
|
||||
size: [number, number, number],
|
||||
seed: string,
|
||||
seed: bigint,
|
||||
version: VersionId,
|
||||
}
|
||||
export function decorator(state: any, img: ImageData, options: DecoratorOptions) {
|
||||
const random = seedrandom(options.seed)
|
||||
const random = new Random(options.seed)
|
||||
const ctx: PlacementContext = {
|
||||
placements: [],
|
||||
features: [],
|
||||
random,
|
||||
biomeInfoNoise: new PerlinNoise(options.seed + 'frwynup', 0, [1]),
|
||||
biomeInfoNoise: new PerlinNoise(random.fork(), 0, [1]),
|
||||
seaLevel: 63,
|
||||
version: options.version,
|
||||
nextFloat: () => random.nextFloat(),
|
||||
nextInt: (max: number) => random.nextInt(max),
|
||||
sampleInt(value) { return sampleInt(value, this) },
|
||||
}
|
||||
|
||||
for (let x = 0; x < options.size[0] / 16; x += 1) {
|
||||
@@ -80,20 +85,16 @@ function decorateY(pos: BlockPos, y: number): BlockPos[] {
|
||||
return [[ pos[0], y, pos[2] ]]
|
||||
}
|
||||
|
||||
function nextInt(max: number, ctx: PlacementContext): number {
|
||||
return Math.floor(ctx.random() * max)
|
||||
}
|
||||
|
||||
function sampleInt(value: any, ctx: PlacementContext): number {
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
} else if (value.base) {
|
||||
return value.base ?? 1 + nextInt(1 + (value.spread ?? 0), ctx)
|
||||
return value.base ?? 1 + ctx.nextInt(1 + (value.spread ?? 0))
|
||||
} else {
|
||||
switch (normalize(value.type)) {
|
||||
case 'constant': return value.value
|
||||
case 'uniform': return value.value.min_inclusive + nextInt(value.value.max_inclusive - value.value.min_inclusive + 1, ctx)
|
||||
case 'biased_to_bottom': return value.value.min_inclusive + nextInt(nextInt(value.value.max_inclusive - value.value.min_inclusive + 1, ctx) + 1, ctx)
|
||||
case 'uniform': return value.value.min_inclusive + ctx.nextInt(value.value.max_inclusive - value.value.min_inclusive + 1)
|
||||
case 'biased_to_bottom': return value.value.min_inclusive + ctx.nextInt(ctx.nextInt(value.value.max_inclusive - value.value.min_inclusive + 1) + 1)
|
||||
case 'clamped': return Math.max(value.value.min_inclusive, Math.min(value.value.max_inclusive, sampleInt(value.value.source, ctx)))
|
||||
}
|
||||
return 1
|
||||
@@ -138,12 +139,12 @@ const Features: {
|
||||
positions.forEach(p => getPlacements(p, config?.feature, ctx))
|
||||
},
|
||||
random_boolean_selector: (config, pos, ctx) => {
|
||||
const feature = ctx.random() < 0.5 ? config?.feature_true : config?.feature_false
|
||||
const feature = ctx.nextFloat() < 0.5 ? config?.feature_true : config?.feature_false
|
||||
getPlacements(pos, feature, ctx)
|
||||
},
|
||||
random_selector: (config, pos, ctx) => {
|
||||
for (const f of config?.features ?? []) {
|
||||
if (ctx.random() < (f?.chance ?? 0)) {
|
||||
if (ctx.nextFloat() < (f?.chance ?? 0)) {
|
||||
getPlacements(pos, f.feature, ctx)
|
||||
return
|
||||
}
|
||||
@@ -151,7 +152,7 @@ const Features: {
|
||||
getPlacements(pos, config?.default, ctx)
|
||||
},
|
||||
simple_random_selector: (config, pos, ctx) => {
|
||||
const feature = config?.features?.[nextInt(config?.features?.length ?? 0, ctx)]
|
||||
const feature = config?.features?.[ctx.nextInt(config?.features?.length ?? 0)]
|
||||
getPlacements(pos, feature, ctx)
|
||||
},
|
||||
}
|
||||
@@ -160,42 +161,42 @@ const Decorators: {
|
||||
[key: string]: (config: any, pos: BlockPos, ctx: PlacementContext) => BlockPos[],
|
||||
} = {
|
||||
chance: (config, pos, ctx) => {
|
||||
return ctx.random() < 1 / (config?.chance ?? 1) ? [pos] : []
|
||||
return ctx.nextFloat() < 1 / (config?.chance ?? 1) ? [pos] : []
|
||||
},
|
||||
count: (config, pos, ctx) => {
|
||||
return new Array(sampleInt(config?.count ?? 1, ctx)).fill(pos)
|
||||
return new Array(ctx.sampleInt(config?.count ?? 1)).fill(pos)
|
||||
},
|
||||
count_extra: (config, pos, ctx) => {
|
||||
let count = config?.count ?? 1
|
||||
if (ctx.random() < config.extra_chance ?? 0){
|
||||
if (ctx.nextFloat() < config.extra_chance ?? 0){
|
||||
count += config.extra_count ?? 0
|
||||
}
|
||||
return new Array(count).fill(pos)
|
||||
},
|
||||
count_multilayer: (config, pos, ctx) => {
|
||||
return new Array(sampleInt(config?.count ?? 1, ctx)).fill(pos)
|
||||
return new Array(ctx.sampleInt(config?.count ?? 1)).fill(pos)
|
||||
.map(p => [
|
||||
p[0] + nextInt(16, ctx),
|
||||
p[0] + ctx.nextInt(16),
|
||||
p[1],
|
||||
p[2] + nextInt(16, ctx),
|
||||
p[2] + ctx.nextInt(16),
|
||||
])
|
||||
},
|
||||
count_noise: (config, pos, ctx) => {
|
||||
const noise = ctx.biomeInfoNoise.getValue(pos[0] / 200, 0, pos[2] / 200)
|
||||
const noise = ctx.biomeInfoNoise.sample(pos[0] / 200, 0, pos[2] / 200)
|
||||
const count = noise < config.noise_level ? config.below_noise : config.above_noise
|
||||
return new Array(count).fill(pos)
|
||||
},
|
||||
count_noise_biased: (config, pos, ctx) => {
|
||||
const factor = Math.max(1, config.noise_factor)
|
||||
const noise = ctx.biomeInfoNoise.getValue(pos[0] / factor, 0, pos[2] / factor)
|
||||
const noise = ctx.biomeInfoNoise.sample(pos[0] / factor, 0, pos[2] / factor)
|
||||
const count = Math.max(0, Math.ceil((noise + (config.noise_offset ?? 0)) * config.noise_to_count_ratio))
|
||||
return new Array(count).fill(pos)
|
||||
},
|
||||
dark_oak_tree: (_config, pos, ctx) => {
|
||||
return [...new Array(16)].map((_, i) => {
|
||||
const x = Math.floor(i / 4) * 4 + 1 + nextInt(3, ctx) + pos[0]
|
||||
const x = Math.floor(i / 4) * 4 + 1 + ctx.nextInt(3) + pos[0]
|
||||
const y = Math.max(ctx.seaLevel, terrain[clamp(0, 63, x)])
|
||||
const z = Math.floor(i % 4) * 4 + 1 + nextInt(3, ctx) + pos[2]
|
||||
const z = Math.floor(i % 4) * 4 + 1 + ctx.nextInt(3) + pos[2]
|
||||
return [x, y, z]
|
||||
})
|
||||
},
|
||||
@@ -205,31 +206,31 @@ const Decorators: {
|
||||
})
|
||||
},
|
||||
depth_average: (config, pos, ctx) => {
|
||||
const y = nextInt(config?.spread ?? 0, ctx) + nextInt(config?.spread ?? 0, ctx) - (config.spread ?? 0) + (config?.baseline ?? 0)
|
||||
const y = ctx.nextInt(config?.spread ?? 0) + ctx.nextInt(config?.spread ?? 0) - (config.spread ?? 0) + (config?.baseline ?? 0)
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
emerald_ore: (_config, pos, ctx) => {
|
||||
const count = 3 + nextInt(6, ctx)
|
||||
const count = 3 + ctx.nextInt(6)
|
||||
return [...new Array(count)].map(() => [
|
||||
pos[0] + nextInt(16, ctx),
|
||||
4 + nextInt(28, ctx),
|
||||
pos[2] + nextInt(16, ctx),
|
||||
pos[0] + ctx.nextInt(16),
|
||||
4 + ctx.nextInt(28),
|
||||
pos[2] + ctx.nextInt(16),
|
||||
])
|
||||
},
|
||||
fire: (config, pos, ctx) => {
|
||||
const count = 1 + nextInt(nextInt(sampleInt(config?.count, ctx), ctx), ctx)
|
||||
const count = 1 + ctx.nextInt(ctx.nextInt(ctx.sampleInt(config?.count)))
|
||||
return [...new Array(count)].map(() => [
|
||||
pos[0] + nextInt(16, ctx),
|
||||
nextInt(128, ctx),
|
||||
pos[2] + nextInt(16, ctx),
|
||||
pos[0] + ctx.nextInt(16),
|
||||
ctx.nextInt(128),
|
||||
pos[2] + ctx.nextInt(16),
|
||||
])
|
||||
},
|
||||
glowstone: (config, pos, ctx) => {
|
||||
const count = nextInt(1 + nextInt(sampleInt(config?.count, ctx), ctx), ctx)
|
||||
const count = ctx.nextInt(1 + ctx.nextInt(ctx.sampleInt(config?.count)))
|
||||
return [...new Array(count)].map(() => [
|
||||
pos[0] + nextInt(16, ctx),
|
||||
nextInt(128, ctx),
|
||||
pos[2] + nextInt(16, ctx),
|
||||
pos[0] + ctx.nextInt(16),
|
||||
ctx.nextInt(128),
|
||||
pos[2] + ctx.nextInt(16),
|
||||
])
|
||||
},
|
||||
heightmap: (_config, pos, ctx) => {
|
||||
@@ -238,7 +239,7 @@ const Decorators: {
|
||||
},
|
||||
heightmap_spread_double: (_config, pos, ctx) => {
|
||||
const y = Math.max(ctx.seaLevel, terrain[clamp(0, 63, pos[0])])
|
||||
return decorateY(pos, nextInt(y * 2, ctx))
|
||||
return decorateY(pos, ctx.nextInt(y * 2))
|
||||
},
|
||||
heightmap_world_surface: (_config, pos, ctx) => {
|
||||
const y = Math.max(ctx.seaLevel, terrain[clamp(0, 63, pos[0])])
|
||||
@@ -246,17 +247,17 @@ const Decorators: {
|
||||
},
|
||||
iceberg: (_config, pos, ctx) => {
|
||||
return [[
|
||||
pos[0] + 4 + nextInt(8, ctx),
|
||||
pos[0] + 4 + ctx.nextInt(8),
|
||||
pos[1],
|
||||
pos[2] + 4 + nextInt(8, ctx),
|
||||
pos[2] + 4 + ctx.nextInt(8),
|
||||
]]
|
||||
},
|
||||
lava_lake: (config, pos, ctx) => {
|
||||
if (nextInt((config.chance ?? 1) / 10, ctx) === 0) {
|
||||
const y = nextInt(nextInt(256 - 8, ctx) + 8, ctx)
|
||||
if (y < ctx.seaLevel || nextInt((config?.chance ?? 1) / 8, ctx) == 0) {
|
||||
const x = nextInt(16, ctx) + pos[0]
|
||||
const z = nextInt(16, ctx) + pos[2]
|
||||
if (ctx.nextInt((config.chance ?? 1) / 10) === 0) {
|
||||
const y = ctx.nextInt(ctx.nextInt(256 - 8) + 8)
|
||||
if (y < ctx.seaLevel || ctx.nextInt((config?.chance ?? 1) / 8) == 0) {
|
||||
const x = ctx.nextInt(16) + pos[0]
|
||||
const z = ctx.nextInt(16) + pos[2]
|
||||
return [[x, y, z]]
|
||||
}
|
||||
}
|
||||
@@ -266,19 +267,19 @@ const Decorators: {
|
||||
return [pos]
|
||||
},
|
||||
range: (config, pos, ctx) => {
|
||||
const y = nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0), ctx) + (config?.bottom_offset ?? 0)
|
||||
const y = ctx.nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0)
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
range_biased: (config, pos, ctx) => {
|
||||
const y = nextInt(nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0), ctx) + (config?.bottom_offset ?? 0), ctx)
|
||||
const y = ctx.nextInt(ctx.nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0))
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
range_very_biased: (config, pos, ctx) => {
|
||||
const y = nextInt(nextInt(nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0), ctx) + (config?.bottom_offset ?? 0), ctx) + (config?.bottom_offset ?? 0), ctx)
|
||||
const y = ctx.nextInt(ctx.nextInt(ctx.nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0)) + (config?.bottom_offset ?? 0))
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
spread_32_above: (_config, pos, ctx) => {
|
||||
const y = nextInt(pos[1] + 32, ctx)
|
||||
const y = ctx.nextInt(pos[1] + 32)
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
top_solid_heightmap: (_config, pos) => {
|
||||
@@ -286,22 +287,28 @@ const Decorators: {
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
magma: (_config, pos, ctx) => {
|
||||
const y = nextInt(pos[1] + 32, ctx)
|
||||
const y = ctx.nextInt(pos[1] + 32)
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
square: (_config, pos, ctx) => {
|
||||
return [[
|
||||
pos[0] + nextInt(16, ctx),
|
||||
pos[0] + ctx.nextInt(16),
|
||||
pos[1],
|
||||
pos[2] + nextInt(16, ctx),
|
||||
pos[2] + ctx.nextInt(16),
|
||||
]]
|
||||
},
|
||||
surface_relative_threshold: (config, pos) => {
|
||||
const height = terrain[clamp(0, 63, pos[0])]
|
||||
const min = height + (config?.min_inclusive ?? -Infinity)
|
||||
const max = height + (config?.max_inclusive ?? Infinity)
|
||||
return (pos[1] < min || pos[1] > max) ? [pos] : []
|
||||
},
|
||||
water_lake: (config, pos, ctx) => {
|
||||
if (nextInt(config.chance ?? 1, ctx) === 0) {
|
||||
if (ctx.nextInt(config.chance ?? 1) === 0) {
|
||||
return [[
|
||||
pos[0] + nextInt(16, ctx),
|
||||
nextInt(256, ctx),
|
||||
pos[2] + nextInt(16, ctx),
|
||||
pos[0] + ctx.nextInt(16),
|
||||
ctx.nextInt(256),
|
||||
pos[2] + ctx.nextInt(16),
|
||||
]]
|
||||
}
|
||||
return []
|
||||
|
||||
@@ -1,21 +1,64 @@
|
||||
import { NoiseChunkGenerator } from './noise/NoiseChunkGenerator'
|
||||
import type { BlockPos, BlockState } from 'deepslate'
|
||||
import { Chunk, ChunkPos, FixedBiome, NoiseChunkGenerator, NoiseGeneratorSettings } from 'deepslate'
|
||||
import type { VersionId } from '../Schemas'
|
||||
import { checkVersion } from '../Schemas'
|
||||
import { deepClone, deepEqual } from '../Utils'
|
||||
import { NoiseChunkGenerator as OldNoiseChunkGenerator } from './noise/NoiseChunkGenerator'
|
||||
|
||||
export type NoiseSettingsOptions = {
|
||||
biomeScale: number,
|
||||
biomeDepth: number,
|
||||
biomeFactor: number,
|
||||
biomeOffset: number,
|
||||
biomePeaks: number,
|
||||
offset: number,
|
||||
width: number,
|
||||
seed: string,
|
||||
seed: bigint,
|
||||
version: VersionId,
|
||||
}
|
||||
|
||||
const Z = 0
|
||||
|
||||
const colors: Record<string, [number, number, number]> = {
|
||||
'minecraft:air': [150, 160, 170],
|
||||
'minecraft:water': [20, 80, 170],
|
||||
'minecraft:lava': [200, 100, 0],
|
||||
'minecraft:stone': [50, 50, 50],
|
||||
'minecraft:netherrack': [100, 40, 40],
|
||||
'minecraft:end_stone': [200, 200, 140],
|
||||
}
|
||||
|
||||
let cacheState: any
|
||||
let generatorCache: NoiseChunkGenerator
|
||||
let chunkCache: Chunk[] = []
|
||||
|
||||
export function noiseSettings(state: any, img: ImageData, options: NoiseSettingsOptions) {
|
||||
const generator = new NoiseChunkGenerator(options.seed)
|
||||
generator.reset(state, options.biomeDepth, options.biomeScale, options.offset, 200)
|
||||
if (checkVersion(options.version, '1.18')) {
|
||||
const { settings, generator } = getCached(state, options)
|
||||
|
||||
const slice = new LevelSlice(-options.offset, options.width, settings.noise.minY, settings.noise.height)
|
||||
slice.fill(generator)
|
||||
|
||||
const data = img.data
|
||||
for (let x = 0; x < options.width; x += 1) {
|
||||
for (let y = 0; y < settings.noise.height; y += 1) {
|
||||
const i = x * 4 + (settings.noise.height-y-1) * 4 * img.width
|
||||
const state = slice.getBlockState([x - options.offset, y, Z])
|
||||
const color = colors[state.getName()] ?? [0, 0, 0]
|
||||
data[i] = color[0]
|
||||
data[i + 1] = color[1]
|
||||
data[i + 2] = color[2]
|
||||
data[i + 3] = 255
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const generator = new OldNoiseChunkGenerator(options.seed)
|
||||
generator.reset(state.noise, options.biomeOffset, options.biomeFactor, options.offset, 200)
|
||||
const data = img.data
|
||||
const row = img.width * 4
|
||||
for (let x = 0; x < options.width; x += 1) {
|
||||
const noise = generator.iterateNoiseColumn(x - options.offset).reverse()
|
||||
for (let y = 0; y < state.height; y += 1) {
|
||||
for (let y = 0; y < state.noise.height; y += 1) {
|
||||
const i = y * row + x * 4
|
||||
const color = getColor(noise, y)
|
||||
data[i] = color
|
||||
@@ -26,6 +69,26 @@ export function noiseSettings(state: any, img: ImageData, options: NoiseSettings
|
||||
}
|
||||
}
|
||||
|
||||
function getCached(state: unknown, options: NoiseSettingsOptions) {
|
||||
const settings = NoiseGeneratorSettings.fromJson(state)
|
||||
// Temporary fix for slides
|
||||
settings.noise.bottomSlide.target *= 128
|
||||
settings.noise.topSlide.target *= 128
|
||||
const shape = { factor: options.biomeFactor, offset: options.biomeOffset, peaks: options.biomePeaks, nearWater: false }
|
||||
|
||||
const newState = [state, shape, `${options.seed}`]
|
||||
if (!deepEqual(newState, cacheState)) {
|
||||
cacheState = deepClone(newState)
|
||||
chunkCache = []
|
||||
const biomeSource = new FixedBiome('unknown')
|
||||
generatorCache = new NoiseChunkGenerator(options.seed, biomeSource, settings, shape)
|
||||
}
|
||||
return {
|
||||
settings,
|
||||
generator: generatorCache,
|
||||
}
|
||||
}
|
||||
|
||||
function getColor(noise: number[], y: number): number {
|
||||
if (noise[y] > 0) {
|
||||
return 0
|
||||
@@ -35,3 +98,42 @@ function getColor(noise: number[], y: number): number {
|
||||
}
|
||||
return 255
|
||||
}
|
||||
|
||||
class LevelSlice {
|
||||
private readonly chunks: Chunk[]
|
||||
private readonly filled: boolean[]
|
||||
|
||||
constructor(
|
||||
private readonly minX: number,
|
||||
width: number,
|
||||
minY: number,
|
||||
height: number,
|
||||
) {
|
||||
this.filled = []
|
||||
this.chunks = [...Array(Math.ceil(width / 16) + 1)]
|
||||
.map((_, i) => {
|
||||
const x = (minX >> 4) + i
|
||||
const cached = chunkCache.find(c => c.pos[0] === x)
|
||||
if (cached) {
|
||||
this.filled[i] = true
|
||||
return cached
|
||||
}
|
||||
return new Chunk(minY, height, ChunkPos.create(x, Z >> 4))
|
||||
})
|
||||
}
|
||||
|
||||
public fill(generator: NoiseChunkGenerator) {
|
||||
this.chunks.forEach((chunk, i) => {
|
||||
if (!this.filled[i]) {
|
||||
generator.fill(chunk)
|
||||
this.filled[i] = true
|
||||
chunkCache.push(chunk)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public getBlockState(pos: BlockPos): BlockState {
|
||||
const chunkIndex = (pos[0] >> 4) - (this.minX >> 4)
|
||||
return this.chunks[chunkIndex].getBlockState(pos)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import type seedrandom from 'seedrandom'
|
||||
import { lerp3, smoothstep } from '../../Utils'
|
||||
|
||||
export class ImprovedNoise {
|
||||
private static readonly GRADIENT = [[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0], [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1], [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1], [1, 1, 0], [0, -1, 1], [-1, 1, 0], [0, -1, -1]]
|
||||
private readonly p: number[]
|
||||
public readonly xo: number
|
||||
public readonly yo: number
|
||||
public readonly zo: number
|
||||
|
||||
constructor(random: seedrandom.prng) {
|
||||
this.xo = random() * 256
|
||||
this.yo = random() * 256
|
||||
this.zo = random() * 256
|
||||
this.p = Array(256)
|
||||
|
||||
for (let i = 0; i < 256; i += 1) {
|
||||
this.p[i] = i
|
||||
}
|
||||
for (let i = 0; i < 256; i += 1) {
|
||||
const n = random.int32() % (256 - i)
|
||||
const b = this.p[i]
|
||||
this.p[i] = this.p[i + n]
|
||||
this.p[i + n] = b
|
||||
}
|
||||
}
|
||||
|
||||
public noise(x: number, y: number, z: number, a: number, b: number) {
|
||||
const x2 = x + this.xo
|
||||
const y2 = y + this.yo
|
||||
const z2 = z + this.zo
|
||||
const x3 = Math.floor(x2)
|
||||
const y3 = Math.floor(y2)
|
||||
const z3 = Math.floor(z2)
|
||||
const x4 = x2 - x3
|
||||
const y4 = y2 - y3
|
||||
const z4 = z2 - z3
|
||||
const x5 = smoothstep(x4)
|
||||
const y5 = smoothstep(y4)
|
||||
const z5 = smoothstep(z4)
|
||||
|
||||
let y6 = 0
|
||||
if (a !== 0) {
|
||||
y6 = Math.floor(Math.min(b, y4) / a) * a
|
||||
}
|
||||
|
||||
return this.sampleAndLerp(x3, y3, z3, x4, y4 - y6, z4, x5, y5, z5)
|
||||
}
|
||||
|
||||
private gradDot(a: number, b: number, c: number, d: number) {
|
||||
const grad = ImprovedNoise.GRADIENT[a & 15]
|
||||
return grad[0] * b + grad[1] * c + grad[2] * d
|
||||
}
|
||||
|
||||
private P(i: number) {
|
||||
return this.p[i & 255] & 255
|
||||
}
|
||||
|
||||
public sampleAndLerp(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) {
|
||||
const j = this.P(a) + b
|
||||
const k = this.P(j) + c
|
||||
const l = this.P(j + 1) + c
|
||||
const m = this.P(a + 1) + b
|
||||
const n = this.P(m) + c
|
||||
const o = this.P(m + 1) + c
|
||||
|
||||
const p = this.gradDot(this.P(k), d, e, f)
|
||||
const q = this.gradDot(this.P(n), d - 1, e, f)
|
||||
const r = this.gradDot(this.P(l), d, e - 1, f)
|
||||
const s = this.gradDot(this.P(o), d - 1, e - 1, f)
|
||||
|
||||
const t = this.gradDot(this.P(k + 1), d, e, f - 1)
|
||||
const u = this.gradDot(this.P(n + 1), d - 1, e, f - 1)
|
||||
const v = this.gradDot(this.P(l + 1), d, e - 1, f - 1)
|
||||
const w = this.gradDot(this.P(o + 1), d - 1, e - 1, f - 1)
|
||||
|
||||
return lerp3(g, h, i, p, q, r, s, t, u, v, w)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PerlinNoise, Random } from 'deepslate'
|
||||
import { clampedLerp, lerp2 } from '../../Utils'
|
||||
import { PerlinNoise } from './PerlinNoise'
|
||||
|
||||
export class NoiseChunkGenerator {
|
||||
private readonly minLimitPerlinNoise: PerlinNoise
|
||||
@@ -17,11 +17,12 @@ export class NoiseChunkGenerator {
|
||||
private noiseColumnCache: (number[] | null)[] = []
|
||||
private xOffset: number = 0
|
||||
|
||||
constructor(seed: string) {
|
||||
this.minLimitPerlinNoise = PerlinNoise.fromRange(seed + 'djfqnqd', -15, 0)
|
||||
this.maxLimitPerlinNoise = PerlinNoise.fromRange(seed + 'gowdnqs', -15, 0)
|
||||
this.mainPerlinNoise = PerlinNoise.fromRange(seed + 'afiwmco', -7, 0)
|
||||
this.depthNoise = PerlinNoise.fromRange(seed + 'qphnmeo', -15, 0)
|
||||
constructor(seed: bigint) {
|
||||
const random = new Random(seed)
|
||||
this.minLimitPerlinNoise = new PerlinNoise(random, -15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
|
||||
this.maxLimitPerlinNoise = new PerlinNoise(random, -15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
|
||||
this.mainPerlinNoise = new PerlinNoise(random, -7, [1, 1, 1, 1, 1, 1, 1, 1])
|
||||
this.depthNoise = new PerlinNoise(random, -15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
|
||||
}
|
||||
|
||||
public reset(settings: any, depth: number, scale: number, xOffset: number, width: number) {
|
||||
@@ -71,7 +72,7 @@ export class NoiseChunkGenerator {
|
||||
const randomDensity = this.settings.random_density_offset ? this.getRandomDensity(x) : 0
|
||||
|
||||
for (let y = 0; y <= this.chunkCountY; y += 1) {
|
||||
let noise = this.sampleAndClampNoise(x, y, this.mainPerlinNoise.getOctaveNoise(0).zo, xzScale, yScale, xzFactor, yFactor)
|
||||
let noise = this.sampleAndClampNoise(x, y, this.mainPerlinNoise.getOctaveNoise(0)!.zo, xzScale, yScale, xzFactor, yFactor)
|
||||
const yOffset = 1 - y * 2 / this.chunkCountY + randomDensity
|
||||
const density = yOffset * this.settings.density_factor + this.settings.density_offset
|
||||
const falloff = (density + this.biomeDepth) * this.biomeScale
|
||||
@@ -100,7 +101,7 @@ export class NoiseChunkGenerator {
|
||||
}
|
||||
|
||||
private getRandomDensity(x: number): number {
|
||||
const noise = this.depthNoise.getValue(x * 200, 10, this.depthNoise.getOctaveNoise(0).zo, 1, 0, true)
|
||||
const noise = this.depthNoise.sample(x * 200, 10, this.depthNoise.getOctaveNoise(0)!.zo, 1, 0, true)
|
||||
const a = (noise < 0) ? -noise * 0.3 : noise
|
||||
const b = a * 24.575625 - 2
|
||||
return (b < 0) ? b * 0.009486607142857142 : Math.min(b, 1) * 0.006640625
|
||||
@@ -120,18 +121,18 @@ export class NoiseChunkGenerator {
|
||||
|
||||
const minLimitNoise = this.minLimitPerlinNoise.getOctaveNoise(i)
|
||||
if (minLimitNoise) {
|
||||
a += minLimitNoise.noise(x2, y2, z2, e, y * e) / d
|
||||
a += minLimitNoise.sample(x2, y2, z2, e, y * e) / d
|
||||
}
|
||||
|
||||
const maxLimitNoise = this.maxLimitPerlinNoise.getOctaveNoise(i)
|
||||
if (maxLimitNoise) {
|
||||
b += maxLimitNoise.noise(x2, y2, z2, e, y * e) / d
|
||||
b += maxLimitNoise.sample(x2, y2, z2, e, y * e) / d
|
||||
}
|
||||
|
||||
if (i < 8) {
|
||||
const mainNoise = this.mainPerlinNoise.getOctaveNoise(i)
|
||||
if (mainNoise) {
|
||||
c += mainNoise.noise(
|
||||
c += mainNoise.sample(
|
||||
PerlinNoise.wrap(x * xzFactor * d),
|
||||
PerlinNoise.wrap(y * yFactor * d),
|
||||
PerlinNoise.wrap(z * xzFactor * d),
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { PerlinNoise } from './PerlinNoise'
|
||||
|
||||
export class NormalNoise {
|
||||
private readonly valueFactor: number
|
||||
private readonly first: PerlinNoise
|
||||
private readonly second: PerlinNoise
|
||||
|
||||
constructor(seed: string, firstOctave: number, amplitudes: number[]) {
|
||||
this.first = new PerlinNoise(seed, firstOctave, amplitudes)
|
||||
this.second = new PerlinNoise(seed + 'a', firstOctave, amplitudes)
|
||||
|
||||
let min = +Infinity
|
||||
let max = -Infinity
|
||||
for (let i = 0; i < amplitudes.length; i += 1) {
|
||||
if (amplitudes[i] !== 0) {
|
||||
min = Math.min(min, i)
|
||||
max = Math.max(max, i)
|
||||
}
|
||||
}
|
||||
|
||||
const expectedDeviation = 0.1 * (1 + 1 / (max - min + 1))
|
||||
this.valueFactor = (1/6) / expectedDeviation
|
||||
}
|
||||
|
||||
getValue(x: number, y: number, z: number) {
|
||||
const x2 = x * 1.0181268882175227
|
||||
const y2 = y * 1.0181268882175227
|
||||
const z2 = z * 1.0181268882175227
|
||||
return (this.first.getValue(x, y, z) + this.second.getValue(x2, y2, z2)) * this.valueFactor
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import seedrandom from 'seedrandom'
|
||||
import { ImprovedNoise } from './ImprovedNoise'
|
||||
|
||||
export class PerlinNoise {
|
||||
private readonly noiseLevels: ImprovedNoise[]
|
||||
private readonly amplitudes: number[]
|
||||
private readonly lowestFreqValueFactor: number
|
||||
private readonly lowestFreqInputFactor: number
|
||||
|
||||
constructor(seed: string, firstOctave: number, amplitudes: number[]) {
|
||||
this.amplitudes = amplitudes
|
||||
|
||||
this.noiseLevels = Array(this.amplitudes.length)
|
||||
for (let i = 0; i < this.amplitudes.length; i += 1) {
|
||||
this.noiseLevels[i] = new ImprovedNoise(seedrandom(seed))
|
||||
}
|
||||
|
||||
this.lowestFreqInputFactor = Math.pow(2, firstOctave)
|
||||
this.lowestFreqValueFactor = Math.pow(2, (amplitudes.length - 1)) / (Math.pow(2, amplitudes.length) - 1)
|
||||
}
|
||||
|
||||
public static fromRange(seed: string, min: number, max: number) {
|
||||
return new PerlinNoise(seed, min, Array(max - min + 1).fill(1))
|
||||
}
|
||||
|
||||
public getValue(x: number, y: number, z: number, a = 0, b = 0, fixY = false) {
|
||||
let value = 0
|
||||
let inputF = this.lowestFreqInputFactor
|
||||
let valueF = this.lowestFreqValueFactor
|
||||
for (let i = 0; i < this.noiseLevels.length; i += 1) {
|
||||
const noise = this.noiseLevels[i]
|
||||
if (noise) {
|
||||
value += this.amplitudes[i] * noise.noise(
|
||||
PerlinNoise.wrap(x * inputF),
|
||||
fixY ? -noise.yo : PerlinNoise.wrap(y * inputF),
|
||||
PerlinNoise.wrap(z * inputF),
|
||||
a * inputF,
|
||||
b * inputF
|
||||
) * valueF
|
||||
}
|
||||
inputF *= 2
|
||||
valueF /= 2
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
public getOctaveNoise(i: number) {
|
||||
return this.noiseLevels[this.noiseLevels.length - 1 - i]
|
||||
}
|
||||
|
||||
public static wrap(value: number) {
|
||||
return value - Math.floor(value / 3.3554432E7 + 0.5) * 3.3554432E7
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user