Voxel rendering + refactor interactive canvas (#322)

* Add voxel rendering to density function preview

* InteractiveCanvas component

* Use interactive canvas for noise preview

* Use interactive canvas for noise settings preview

* Extract common iterateWorld2D logic

* Use InteractiveCanvas2D for biome source preview

* Display final density in noise settings preview hover

* Move remaining preview code

* Hide noise router info for checkerboard and fixed

* Add higher resolution biome map

* User interactive canvas for decorator preview
This commit is contained in:
Misode
2023-01-26 01:21:02 +01:00
committed by GitHub
parent 23b3046dee
commit 00029a2010
32 changed files with 996 additions and 1085 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ColormapType } from './previews/Colormap.js'
import type { ColormapType } from './components/previews/Colormap.js'
import type { VersionId } from './services/index.js'
type Method = 'menu' | 'hotkey'
+2 -2
View File
@@ -1,7 +1,7 @@
import type { ColormapType } from './components/previews/Colormap.js'
import { ColormapTypes } from './components/previews/Colormap.js'
import type { Project } from './contexts/index.js'
import { DRAFT_PROJECT } from './contexts/index.js'
import type { ColormapType } from './previews/Colormap.js'
import { ColormapTypes } from './previews/Colormap.js'
import type { VersionId } from './services/index.js'
import { DEFAULT_VERSION, VersionIds } from './services/index.js'
+21
View File
@@ -2,6 +2,8 @@ import type { DataModel } from '@mcschema/core'
import { Path } from '@mcschema/core'
import * as zip from '@zip.js/zip.js'
import type { Random } from 'deepslate/core'
import type { mat3 } from 'gl-matrix'
import { vec2 } from 'gl-matrix'
import yaml from 'js-yaml'
import { route } from 'preact-router'
import rfdc from 'rfdc'
@@ -356,3 +358,22 @@ export function getWeightedRandom<T>(random: Random, entries: T[], getWeight: (e
}
return undefined
}
export function iterateWorld2D<D>(img: ImageData, transform: mat3, getData: (x: number, y: number) => D, getColor: (d: D) => [number, number, number]) {
const pos = vec2.create()
const arr = Array(img.width * img.height)
for (let x = 0; x < img.width; x += 1) {
for (let y = 0; y < img.height; y += 1) {
const i = x + y * img.width
vec2.transformMat3(pos, vec2.fromValues(x, y), transform)
arr[i] = getData(Math.floor(pos[0]), -Math.floor(pos[1]))
}
}
for (let i = 0; i < img.width * img.height; i += 1) {
const color = getColor(arr[i])
img.data[4 * i] = color[0]
img.data[4 * i + 1] = color[1]
img.data[4 * i + 2] = color[2]
img.data[4 * i + 3] = 255
}
}
+1 -1
View File
@@ -3,11 +3,11 @@ import { Identifier } from 'deepslate/core'
import { useEffect, useRef, useState } from 'preact/hooks'
import { useVersion } from '../contexts/Version.jsx'
import { useAsync } from '../hooks/useAsync.js'
import { itemHasGlint } from '../previews/LootTable.js'
import { renderItem } from '../services/Resources.js'
import { getCollections } from '../services/Schemas.js'
import { ItemTooltip } from './ItemTooltip.jsx'
import { Octicon } from './Octicon.jsx'
import { itemHasGlint } from './previews/LootTable.js'
interface Props {
item: ItemStack,
+9 -13
View File
@@ -24,35 +24,31 @@ export function PreviewPanel({ model, version, id, shown }: PreviewPanelProps) {
})
if (!model) return <></>
const data = model.get(new Path([]))
if (!data) return <></>
if (id === 'loot_table') {
const data = model.get(new Path([]))
if (data) return <LootTablePreview {...{ model, version, shown, data }} />
return <LootTablePreview {...{ model, version, shown, data }} />
}
if (id === 'dimension' && model.get(new Path(['generator', 'type']))?.endsWith('noise')) {
const data = model.get(new Path(['generator', 'biome_source']))
if (data) return <BiomeSourcePreview {...{ model, version, shown, data }} />
return <BiomeSourcePreview {...{ model, version, shown, data }} />
}
if (id === 'worldgen/density_function') {
const data = model.get(new Path([]))
if (data) return <DensityFunctionPreview {...{ model, version, shown, data }} />
return <DensityFunctionPreview {...{ model, version, shown, data }} />
}
if (id === 'worldgen/noise') {
const data = model.get(new Path([]))
if (data) return <NoisePreview {...{ model, version, shown, data }} />
return <NoisePreview {...{ model, version, shown, data }} />
}
if (id === 'worldgen/noise_settings') {
const data = model.get(new Path([]))
if (data) return <NoiseSettingsPreview {...{ model, version, shown, data }} />
if (id === 'worldgen/noise_settings' && checkVersion(version, '1.18')) {
return <NoiseSettingsPreview {...{ model, version, shown, data }} />
}
if ((id === 'worldgen/placed_feature' || (id === 'worldgen/configured_feature' && checkVersion(version, '1.16', '1.17')))) {
const data = model.get(new Path([]))
if (data) return <DecoratorPreview {...{ model, version, shown, data }} />
return <DecoratorPreview {...{ model, version, shown, data }} />
}
return <></>
@@ -1,99 +1,273 @@
import { DataModel, Path } from '@mcschema/core'
import { useEffect, useRef, useState } from 'preact/hooks'
import { useLocale, useProject, useStore } from '../../contexts/index.js'
import { useCanvas } from '../../hooks/index.js'
import { biomeMap, getBiome } from '../../previews/index.js'
import { randomSeed } from '../../Utils.js'
import { DataModel } from '@mcschema/core'
import { clampedMap } from 'deepslate'
import { mat3 } from 'gl-matrix'
import { useCallback, useRef, useState } from 'preact/hooks'
import { getProjectData, useLocale, useProject, useStore } from '../../contexts/index.js'
import { useAsync } from '../../hooks/index.js'
import { checkVersion } from '../../services/Schemas.js'
import { Store } from '../../Store.js'
import { iterateWorld2D, randomSeed, stringToColor } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
import { ColormapSelector } from './ColormapSelector.jsx'
import { DEEPSLATE } from './Deepslate.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps) => {
const LAYERS = ['biomes', 'temperature', 'vegetation', 'continents', 'erosion', 'ridges', 'depth'] as const
type Layer = typeof LAYERS[number]
const DETAIL_DELAY = 300
const DETAIL_SCALE = 2
export const BiomeSourcePreview = ({ data, shown, version }: PreviewProps) => {
const { locale } = useLocale()
const { project } = useProject()
const [seed, setSeed] = useState(randomSeed())
const [scale, setScale] = useState(2)
const [yOffset, setYOffset] = useState(64)
const [focused, setFocused] = useState<{[k: string]: number | string} | undefined>(undefined)
const { biomeColors } = useStore()
const offset = useRef<[number, number]>([0, 0])
const res = useRef(1)
const refineTimeout = useRef<number>()
const [seed, setSeed] = useState(randomSeed())
const [layer, setLayer] = useState<Layer>('biomes')
const [yOffset, setYOffset] = useState(64)
const [focused, setFocused] = useState<string[]>([])
const [focused2, setFocused2] = useState<string[]>([])
const settings = DataModel.unwrapLists(model.get(new Path(['generator', 'settings'])))
const state = JSON.stringify([data, settings])
const type: string = data.type?.replace(/^minecraft:/, '')
const state = JSON.stringify(data)
const type: string = data?.generator?.biome_source?.type?.replace(/^minecraft:/, '') ?? ''
const hasRandomness = type === 'multi_noise' || type === 'the_end'
const { canvas, redraw } = useCanvas({
size() {
return [200 / res.current, 200 / res.current]
},
async draw(img) {
const options = { settings, biomeColors, offset: offset.current, scale, seed, res: res.current, version, project, y: yOffset }
await biomeMap(data, img, options)
if (res.current === 4) {
clearTimeout(refineTimeout.current)
refineTimeout.current = setTimeout(() => {
res.current = 1
redraw()
}, 150) as any
}
},
async onDrag(dx, dy) {
offset.current[0] = offset.current[0] + dx * 200
offset.current[1] = offset.current[1] + dy * 200
clearTimeout(refineTimeout.current)
res.current = hasRandomness ? 4 : 1
redraw()
},
async onHover(x, y) {
const options = { settings, biomeColors, offset: offset.current, scale, seed: seed, res: 1, version, project, y: yOffset }
const biome = await getBiome(data, Math.floor(x * 200), Math.floor(y * 200), options)
setFocused(biome)
},
onLeave() {
setFocused(undefined)
},
}, [version, state, scale, seed, yOffset, biomeColors, project])
useEffect(() => {
if (shown) {
res.current = hasRandomness ? 4 : 1
redraw()
const { value } = useAsync(async function loadBiomeSource() {
await DEEPSLATE.loadVersion(version, getProjectData(project))
await DEEPSLATE.loadChunkGenerator(DataModel.unwrapLists(data?.generator?.settings), DataModel.unwrapLists(data?.generator?.biome_source), seed)
return {
biomeSource: { loaded: true },
noiseRouter: checkVersion(version, '1.19') ? DEEPSLATE.getNoiseRouter() : undefined,
}
}, [version, state, scale, seed, yOffset, shown, biomeColors, project])
}, [state, seed, project, version])
const { biomeSource, noiseRouter } = value ?? {}
const changeScale = (newScale: number) => {
newScale = Math.max(1, Math.round(newScale))
offset.current[0] = offset.current[0] * scale / newScale
offset.current[1] = offset.current[1] * scale / newScale
setScale(newScale)
}
const actualLayer = noiseRouter ? layer : 'biomes'
const ctx = useRef<CanvasRenderingContext2D>()
const imageData = useRef<ImageData>()
const [colormap, setColormap] = useState<ColormapType>(Store.getColormap() ?? 'viridis')
const detailCanvas = useRef<HTMLCanvasElement>(null)
const detailCtx = useRef<CanvasRenderingContext2D>()
const detailImageData = useRef<ImageData>()
const detailTimeout = useRef<number>()
const onSetup = useCallback(function onSetup(canvas: HTMLCanvasElement) {
ctx.current = canvas.getContext('2d') ?? undefined
detailCtx.current = detailCanvas.current?.getContext('2d') ?? undefined
}, [])
const onResize = useCallback(function onResize(width: number, height: number) {
if (ctx.current) {
imageData.current = ctx.current.getImageData(0, 0, width, height)
}
if (detailCtx.current && detailCanvas.current) {
detailCanvas.current.width = width * DETAIL_SCALE
detailCanvas.current.height = height * DETAIL_SCALE
detailImageData.current = detailCtx.current.getImageData(0, 0, width * DETAIL_SCALE, height * DETAIL_SCALE)
}
}, [])
const onDraw = useCallback(function onDraw(transform: mat3) {
if (!ctx.current || !imageData.current || !shown) return
function actualDraw(ctx: CanvasRenderingContext2D, img: ImageData, transform: mat3) {
if (actualLayer === 'biomes' && biomeSource) {
iterateWorld2D(img, transform, (x, y) => {
return DEEPSLATE.getBiome(x, yOffset, y)
}, (biome) => {
return getBiomeColor(biome, biomeColors)
})
} else if (actualLayer !== 'biomes' && noiseRouter) {
const df = noiseRouter[actualLayer]
const colorPicker = getColormap(colormap)
iterateWorld2D(img, transform, (x, y) => {
return df.compute({ x: x*4, y: yOffset, z: y*4 }) ?? 0
}, (density) => {
const color = colorPicker(clampedMap(density, -1, 1, 0, 1))
return [color[0] * 256, color[1] * 256, color[2] * 256]
})
}
ctx.putImageData(img, 0, 0)
}
actualDraw(ctx.current, imageData.current, transform)
detailCanvas.current?.classList.remove('visible')
clearTimeout(detailTimeout.current)
if (hasRandomness) {
detailTimeout.current = setTimeout(function detailTimout() {
if (!detailCtx.current || !detailImageData.current || !detailCanvas.current) return
const detailTransform = mat3.create()
mat3.scale(detailTransform, transform, [1/DETAIL_SCALE, 1/DETAIL_SCALE])
actualDraw(detailCtx.current, detailImageData.current, detailTransform)
detailCanvas.current.classList.add('visible')
}, DETAIL_DELAY) as unknown as number
}
}, [biomeSource, noiseRouter, actualLayer, colormap, shown, biomeColors, yOffset])
const onHover = useCallback(function onHover(pos: [number, number] | undefined) {
const [x, y] = pos ?? [0, 0]
if (!pos || !biomeSource) {
setFocused([])
} else {
const biome = DEEPSLATE.getBiome(x, yOffset, -y)
setFocused([biome.replace(/^minecraft:/, ''), `X=${x*4} Z=${-y*4}`])
}
if (!pos || !noiseRouter) {
setFocused2([])
} else {
setFocused2([LAYERS.flatMap(l => {
if (l === 'biomes') return []
const value = noiseRouter[l].compute({ x: x*4, y: yOffset, z: -y*4 })
return [`${locale(`layer.${l}`).charAt(0)}=${value.toPrecision(2)}`]
}).join(' ')])
}
}, [biomeSource, noiseRouter, yOffset])
return <>
{(hasRandomness && focused2) && <div class="controls secondary-controls">
{focused2.map(s => <Btn label={s} class="no-pointer" /> )}
</div>}
<div class="controls preview-controls">
{focused && <Btn label={focused.biome as string} class="no-pointer" />}
<Btn icon="dash" tooltip={locale('zoom_out')}
onClick={() => changeScale(scale * 2)} />
<Btn icon="plus" tooltip={locale(Math.round(scale) <= 1 ? 'zoom_in_limit' : 'zoom_in')}
disabled={Math.round(scale) <= 1}
onClick={() => changeScale(scale / 2)} />
{focused.map(s => <Btn label={s} class="no-pointer" /> )}
{actualLayer !== 'biomes' && <ColormapSelector value={colormap} onChange={setColormap} />}
{hasRandomness && <>
<BtnMenu icon="stack">
<BtnMenu icon="stack" tooltip={locale('layer')}>
<div class="btn btn-input" onClick={e => e.stopPropagation()}>
<span>{locale('y')}</span>
<NumberInput value={yOffset} onChange={setYOffset} />
</div>
{checkVersion(version, '1.19') && LAYERS.map(l => <Btn label={locale(`layer.${l}`)} active={l === actualLayer} onClick={() => setLayer(l)} />)}
</BtnMenu>
<Btn icon="sync" tooltip={locale('generate_new_seed')}
onClick={() => setSeed(randomSeed())} />
</>}
</div>
{focused?.temperature !== undefined && <div class="controls secondary-controls">
<Btn class="no-pointer" label={Object.entries(focused)
.filter(([k]) => k !== 'biome')
.map(([k, v]) => `${k[0].toUpperCase()}: ${(v as number).toFixed(2)}`).join(' ')}/>
</div>}
<canvas ref={canvas} width="200" height="200"></canvas>
<div class="full-preview">
<InteractiveCanvas2D onSetup={onSetup} onResize={onResize} onDraw={onDraw} onHover={onHover} pixelSize={hasRandomness ? 8 : 2} />
{hasRandomness && <canvas class={'preview-details'} ref={detailCanvas} />}
</div>
</>
}
type Triple = [number, number, number]
type BiomeColors = Record<string, Triple>
function getBiomeColor(biome: string, biomeColors: BiomeColors): Triple {
if (!biome) {
return [128, 128, 128]
}
const color = biomeColors[biome] ?? VanillaColors[biome]
if (color === undefined) {
return stringToColor(biome)
}
return color
}
export const VanillaColors: Record<string, Triple> = {
'minecraft:badlands': [217,69,21],
'minecraft:badlands_plateau': [202,140,101],
'minecraft:bamboo_jungle': [118,142,20],
'minecraft:bamboo_jungle_hills': [59,71,10],
'minecraft:basalt_deltas': [64,54,54],
'minecraft:beach': [250,222,85],
'minecraft:birch_forest': [48,116,68],
'minecraft:birch_forest_hills': [31,95,50],
'minecraft:cold_ocean': [32,32,112],
'minecraft:crimson_forest': [221,8,8],
'minecraft:dark_forest': [64,81,26],
'minecraft:dark_forest_hills': [104,121,66],
'minecraft:deep_cold_ocean': [32,32,56],
'minecraft:deep_frozen_ocean': [64,64,144],
'minecraft:deep_lukewarm_ocean': [0,0,64],
'minecraft:deep_ocean': [0,0,48],
'minecraft:deep_warm_ocean': [0,0,80],
'minecraft:desert': [250,148,24],
'minecraft:desert_hills': [210,95,18],
'minecraft:desert_lakes': [255,188,64],
'minecraft:end_barrens': [39,30,61],
'minecraft:end_highlands': [232,244,178],
'minecraft:end_midlands': [194,187,136],
'minecraft:eroded_badlands': [255,109,61],
'minecraft:flower_forest': [45,142,73],
'minecraft:forest': [5,102,33],
'minecraft:frozen_ocean': [112,112,214],
'minecraft:frozen_river': [160,160,255],
'minecraft:giant_spruce_taiga': [129,142,121],
'minecraft:old_growth_spruce_taiga': [129,142,121],
'minecraft:giant_spruce_taiga_hills': [109,119,102],
'minecraft:giant_tree_taiga': [89,102,81],
'minecraft:old_growth_pine_taiga': [89,102,81],
'minecraft:giant_tree_taiga_hills': [69,79,62],
'minecraft:gravelly_hills': [136,136,136],
'minecraft:gravelly_mountains': [136,136,136],
'minecraft:windswept_gravelly_hills': [136,136,136],
'minecraft:ice_spikes': [180,220,220],
'minecraft:jungle': [83,123,9],
'minecraft:jungle_edge': [98,139,23],
'minecraft:sparse_jungle': [98,139,23],
'minecraft:jungle_hills': [44,66,5],
'minecraft:lukewarm_ocean': [0,0,144],
'minecraft:modified_badlands_plateau': [242,180,141],
'minecraft:modified_gravelly_mountains': [120,152,120],
'minecraft:modified_jungle': [123,163,49],
'minecraft:modified_jungle_edge': [138,179,63],
'minecraft:modified_wooded_badlands_plateau': [216,191,141],
'minecraft:mountain_edge': [114,120,154],
'minecraft:extreme_hills': [96,96,96],
'minecraft:mountains': [96,96,96],
'minecraft:windswept_hills': [96,96,96],
'minecraft:mushroom_field_shore': [160,0,255],
'minecraft:mushroom_fields': [255,0,255],
'minecraft:nether_wastes': [191,59,59],
'minecraft:ocean': [0,0,112],
'minecraft:plains': [141,179,96],
'minecraft:river': [0,0,255],
'minecraft:savanna': [189,178,95],
'minecraft:savanna_plateau': [167,157,100],
'minecraft:shattered_savanna': [229,218,135],
'minecraft:windswept_savanna': [229,218,135],
'minecraft:shattered_savanna_plateau': [207,197,140],
'minecraft:small_end_islands': [16,12,28],
'minecraft:snowy_beach': [250,240,192],
'minecraft:snowy_mountains': [160,160,160],
'minecraft:snowy_taiga': [49,85,74],
'minecraft:snowy_taiga_hills': [36,63,54],
'minecraft:snowy_taiga_mountains': [89,125,114],
'minecraft:snowy_tundra': [255,255,255],
'minecraft:snowy_plains': [255,255,255],
'minecraft:soul_sand_valley': [94,56,48],
'minecraft:stone_shore': [162,162,132],
'minecraft:stony_shore': [162,162,132],
'minecraft:sunflower_plains': [181,219,136],
'minecraft:swamp': [7,249,178],
'minecraft:swamp_hills': [47,255,218],
'minecraft:taiga': [11,102,89],
'minecraft:taiga_hills': [22,57,51],
'minecraft:taiga_mountains': [51,142,129],
'minecraft:tall_birch_forest': [88,156,108],
'minecraft:old_growth_birch_forest': [88,156,108],
'minecraft:tall_birch_hills': [71,135,90],
'minecraft:the_end': [59,39,84],
'minecraft:the_void': [0,0,0],
'minecraft:warm_ocean': [0,0,172],
'minecraft:warped_forest': [73,144,123],
'minecraft:wooded_badlands_plateau': [176,151,101],
'minecraft:wooded_badlands': [176,151,101],
'minecraft:wooded_hills': [34,85,28],
'minecraft:wooded_mountains': [80,112,80],
'minecraft:windswept_forest': [80,112,80],
'minecraft:snowy_slopes': [140, 195, 222],
'minecraft:lofty_peaks': [196, 168, 193],
'minecraft:jagged_peaks': [196, 168, 193],
'minecraft:snowcapped_peaks': [200, 198, 200],
'minecraft:frozen_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],
'minecraft:deep_dark': [10, 14, 19],
'minecraft:mangrove_swamp': [36,196,142],
}
@@ -1,4 +1,4 @@
import { clamp } from '../Utils.js'
import { clamp } from '../../Utils.js'
import colormaps from './colormaps.json'
// Implementation based on https://github.com/politiken-journalism/scale-color-perceptual/blob/master/utils/interpolate.js
@@ -1,10 +1,10 @@
import { useCallback } from 'preact/hooks'
import { Analytics } from '../../Analytics.js'
import type { ColormapType } from '../../previews/Colormap.js'
import { ColormapTypes } from '../../previews/Colormap.js'
import { Store } from '../../Store.js'
import { Btn } from '../Btn.jsx'
import { BtnMenu } from '../BtnMenu.jsx'
import type { ColormapType } from './Colormap.js'
import { ColormapTypes } from './Colormap.js'
interface Props {
value: ColormapType,
@@ -1,14 +1,13 @@
import { DataModel } from '@mcschema/core'
import type { Random } from 'deepslate/worldgen'
import { LegacyRandom, PerlinNoise } from 'deepslate/worldgen'
import type { VersionId } from '../services/index.js'
import { checkVersion } from '../services/index.js'
import { clamp, isObject, stringToColor } from '../Utils.js'
import type { BlockPos, ChunkPos, PerlinNoise, Random } from 'deepslate/worldgen'
import type { VersionId } from '../../services/index.js'
import { checkVersion } from '../../services/index.js'
import type { Color } from '../../Utils.js'
import { clamp, isObject, stringToColor } from '../../Utils.js'
type BlockPos = [number, number, number]
type Placement = [BlockPos, number]
export type Placement = [BlockPos, number]
type PlacementContext = {
export type PlacementContext = {
placements: Placement[],
features: string[],
random: Random,
@@ -18,12 +17,17 @@ type PlacementContext = {
nextFloat(): number,
nextInt(max: number): number,
nextGaussian(): number,
sampleInt(provider: any): number,
}
export type PlacedFeature = {
pos: BlockPos,
feature: string,
color: Color,
}
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]
const featureColors = [
const featureColors: Color[] = [
[255, 77, 54], // red
[59, 118, 255], // blue
[91, 207, 25], // green
@@ -32,58 +36,22 @@ const featureColors = [
[52, 204, 209], // cyan
]
export type DecoratorOptions = {
size: [number, number, number],
seed: bigint,
version: VersionId,
}
export function decorator(state: any, img: ImageData, options: DecoratorOptions) {
const random = new LegacyRandom(options.seed)
const ctx: PlacementContext = {
placements: [],
features: [],
random,
biomeInfoNoise: new PerlinNoise(random.fork(), 0, [1]),
seaLevel: 63,
version: options.version,
nextFloat: () => random.nextFloat(),
nextInt: (max: number) => random.nextInt(max),
nextGaussian: () => Math.sqrt(-2 * Math.log(1 - random.nextFloat())) * Math.cos(2 * Math.PI * random.nextFloat()),
sampleInt(value) { return sampleInt(value, this) },
export function decorateChunk(pos: ChunkPos, state: any, ctx: PlacementContext): PlacedFeature[] {
if (checkVersion(ctx.version, undefined, '1.17')) {
getPlacements([pos[0] * 16, 0, pos[1] * 16], DataModel.unwrapLists(state), ctx)
} else {
modifyPlacement([pos[0] * 16, 0, pos[1] * 16], DataModel.unwrapLists(state.placement), ctx)
}
for (let x = 0; x < options.size[0] / 16; x += 1) {
for (let z = 0; z < options.size[2] / 16; z += 1) {
if (checkVersion(options.version, undefined, '1.17')) {
getPlacements([x * 16, 0, z * 16], DataModel.unwrapLists(state), ctx)
} else {
modifyPlacement([x * 16, 0, z * 16], DataModel.unwrapLists(state.placement), ctx)
}
return ctx.placements.map(([pos, i]) => {
const feature = ctx.features[i]
let color = i < featureColors.length ? featureColors[i] : stringToColor(feature)
color = [clamp(color[0], 50, 205), clamp(color[1], 50, 205), clamp(color[2], 50, 205)]
if ((Math.floor(pos[0] / 16) + Math.floor(pos[2] / 16)) % 2 === 0) {
color = [0.85 * color[0], 0.85 * color[1], 0.85 * color[2]]
}
}
const data = img.data
img.data.fill(255)
for (const [pos, feature] of ctx.placements) {
if (pos[0] < 0 || pos[1] < 0 || pos[2] < 0 || pos[0] >= options.size[0] || pos[1] >= options.size[1] || pos[2] >= options.size[2]) continue
const i = (pos[2] * (img.width * 4)) + (pos[0] * 4)
const color = feature < featureColors.length ? featureColors[feature] : stringToColor(ctx.features[feature])
data[i] = clamp(color[0], 50, 205)
data[i + 1] = clamp(color[1], 50, 205)
data[i + 2] = clamp(color[2], 50, 205)
data[i + 3] = 255
}
for (let x = 0; x < options.size[0]; x += 1) {
for (let y = 0; y < options.size[2]; y += 1) {
if ((Math.floor(x / 16) + Math.floor(y / 16)) % 2 === 0) continue
const i = (y * (img.width * 4)) + (x * 4)
for (let j = 0; j < 3; j += 1) {
data[i + j] = 0.85 * data[i + j]
}
}
}
return { pos, feature, color }
})
}
function normalize(id: string) {
@@ -94,7 +62,7 @@ function decorateY(pos: BlockPos, y: number): BlockPos[] {
return [[ pos[0], y, pos[2] ]]
}
function sampleInt(value: any, ctx: PlacementContext): number {
export function sampleInt(value: any, ctx: PlacementContext): number {
if (typeof value === 'number') {
return value
} else if (value.base) {
@@ -104,7 +72,7 @@ function sampleInt(value: any, ctx: PlacementContext): number {
case 'constant': return value.value
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 clamp(ctx.sampleInt(value.value.source), value.value.min_inclusive, value.value.max_inclusive)
case 'clamped': return clamp(sampleInt(value.value.source, ctx), value.value.min_inclusive, value.value.max_inclusive)
case 'clamped_normal':
const normal = value.value.mean + ctx.nextGaussian() * value.value.deviation
return Math.floor(clamp(value.value.min_inclusive, value.value.max_inclusive, normal))
@@ -113,7 +81,7 @@ function sampleInt(value: any, ctx: PlacementContext): number {
let i = ctx.nextInt(totalWeight)
for (const e of value.distribution) {
i -= e.weight
if (i < 0) return ctx.sampleInt(e.data)
if (i < 0) return sampleInt(e.data, ctx)
}
return 0
}
@@ -122,11 +90,11 @@ function sampleInt(value: any, ctx: PlacementContext): number {
}
function resolveAnchor(anchor: any, _ctx: PlacementContext): number {
if (!isObject(anchor)) throw new Error('Invalid vertical anchor')
if (anchor.absolute) return anchor.absolute
if (anchor.above_bottom) return anchor.above_bottom
if (anchor.below_top) return 256 - anchor.below_top
throw new Error('Invalid vertical anchor')
if (!isObject(anchor)) return 0
if (anchor.absolute !== undefined) return anchor.absolute
if (anchor.above_bottom !== undefined) return anchor.above_bottom
if (anchor.below_top !== undefined) return 256 - anchor.below_top
return 0
}
function sampleHeight(height: any, ctx: PlacementContext): number {
@@ -234,7 +202,7 @@ const Decorators: {
return ctx.nextFloat() < 1 / (config?.chance ?? 1) ? [pos] : []
},
count: (config, pos, ctx) => {
return new Array(ctx.sampleInt(config?.count ?? 1)).fill(pos)
return new Array(sampleInt(config?.count ?? 1, ctx)).fill(pos)
},
count_extra: (config, pos, ctx) => {
let count = config?.count ?? 1
@@ -244,7 +212,7 @@ const Decorators: {
return new Array(count).fill(pos)
},
count_multilayer: (config, pos, ctx) => {
return new Array(ctx.sampleInt(config?.count ?? 1)).fill(pos)
return new Array(sampleInt(config?.count ?? 1, ctx)).fill(pos)
.map(p => [
p[0] + ctx.nextInt(16),
p[1],
@@ -288,7 +256,7 @@ const Decorators: {
])
},
fire: (config, pos, ctx) => {
const count = 1 + ctx.nextInt(ctx.nextInt(ctx.sampleInt(config?.count)))
const count = 1 + ctx.nextInt(ctx.nextInt(sampleInt(config?.count, ctx)))
return [...new Array(count)].map(() => [
pos[0] + ctx.nextInt(16),
ctx.nextInt(128),
@@ -296,7 +264,7 @@ const Decorators: {
])
},
glowstone: (config, pos, ctx) => {
const count = ctx.nextInt(1 + ctx.nextInt(ctx.sampleInt(config?.count)))
const count = ctx.nextInt(1 + ctx.nextInt(sampleInt(config?.count, ctx)))
return [...new Array(count)].map(() => [
pos[0] + ctx.nextInt(16),
ctx.nextInt(128),
@@ -404,10 +372,10 @@ const PlacementModifiers: {
[key: string]: (config: any, pos: BlockPos, ctx: PlacementContext) => BlockPos[],
} = {
count: ({ count }, pos, ctx) => {
return new Array(ctx.sampleInt(count ?? 1)).fill(pos)
return new Array(sampleInt(count ?? 1, ctx)).fill(pos)
},
count_on_every_layer: ({ count }, pos, ctx) => {
return new Array(ctx.sampleInt(count ?? 1)).fill(pos)
return new Array(sampleInt(count ?? 1, ctx)).fill(pos)
.map(p => [
p[0] + ctx.nextInt(16),
p[1],
@@ -444,9 +412,9 @@ const PlacementModifiers: {
},
random_offset: ({ xz_spread, y_spread }, pos, ctx) => {
return [[
pos[0] + ctx.sampleInt(xz_spread),
pos[1] + ctx.sampleInt(y_spread),
pos[2] + ctx.sampleInt(xz_spread),
pos[0] + sampleInt(xz_spread, ctx),
pos[1] + sampleInt(y_spread, ctx),
pos[2] + sampleInt(xz_spread, ctx),
]]
},
rarity_filter: ({ chance }, pos, ctx) => {
@@ -1,42 +1,86 @@
import { useEffect, useState } from 'preact/hooks'
import { BlockPos, ChunkPos, LegacyRandom, PerlinNoise } from 'deepslate'
import type { mat3 } from 'gl-matrix'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale } from '../../contexts/index.js'
import { useCanvas } from '../../hooks/index.js'
import { decorator } from '../../previews/index.js'
import { randomSeed } from '../../Utils.js'
import { computeIfAbsent, iterateWorld2D, randomSeed } from '../../Utils.js'
import { Btn } from '../index.js'
import type { PlacedFeature, PlacementContext } from './Decorator.js'
import { decorateChunk } from './Decorator.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
export const DecoratorPreview = ({ data, version, shown }: PreviewProps) => {
const { locale } = useLocale()
const [scale, setScale] = useState(4)
const [seed, setSeed] = useState(randomSeed())
const state = JSON.stringify(data)
const { canvas, redraw } = useCanvas({
size() {
return [scale * 16, scale * 16]
},
async draw(img) {
decorator(data, img, { seed, version, size: [scale * 16, 128, scale * 16] })
},
}, [version, state, seed])
useEffect(() => {
if (shown) {
redraw()
const { context, chunkFeatures } = useMemo(() => {
const random = new LegacyRandom(seed)
const context: PlacementContext = {
placements: [],
features: [],
random,
biomeInfoNoise: new PerlinNoise(random.fork(), 0, [1]),
seaLevel: 63,
version: version,
nextFloat: () => random.nextFloat(),
nextInt: (max: number) => random.nextInt(max),
nextGaussian: () => Math.sqrt(-2 * Math.log(1 - random.nextFloat())) * Math.cos(2 * Math.PI * random.nextFloat()),
}
}, [version, state, scale, seed, shown])
return {
context,
chunkFeatures: new Map<string, PlacedFeature[]>(),
}
}, [state, version, seed])
const ctx = useRef<CanvasRenderingContext2D>()
const imageData = useRef<ImageData>()
const [focused, setFocused] = useState<string[]>([])
const onSetup = useCallback(function onSetup(canvas: HTMLCanvasElement) {
const ctx2D = canvas.getContext('2d')
if (!ctx2D) return
ctx.current = ctx2D
}, [])
const onResize = useCallback(function onResize(width: number, height: number) {
if (!ctx.current) return
imageData.current = ctx.current.getImageData(0, 0, width, height)
}, [])
const onDraw = useCallback(function onDraw(transform: mat3) {
if (!ctx.current || !imageData.current || !shown) return
iterateWorld2D(imageData.current, transform, (x, y) => {
const pos = ChunkPos.create(Math.floor(x / 16), Math.floor(-y / 16))
const features = computeIfAbsent(chunkFeatures, `${pos[0]} ${pos[1]}`, () => decorateChunk(pos, data, context))
return features.find(f => f.pos[0] === x && f.pos[2] == -y) ?? { pos: BlockPos.create(x, 0, -y) }
}, (feature) => {
if ('color' in feature) {
return feature.color
}
if ((Math.floor(feature.pos[0] / 16) + Math.floor(feature.pos[2] / 16)) % 2 === 0) {
return [0.85 * 256, 0.85 * 256, 0.85 * 256]
}
return [256, 256, 256]
})
ctx.current.putImageData(imageData.current, 0, 0)
}, [context, chunkFeatures, shown])
const onHover = useCallback(function onHover(pos: [number, number] | undefined) {
if (!pos) {
setFocused([])
} else {
const [x, y] = pos
setFocused([`X=${x} Z=${-y}`])
}
}, [])
return <>
<div class="controls preview-controls">
<Btn icon="dash" tooltip={locale('zoom_out')}
onClick={() => setScale(Math.min(16, scale + 1))} />
<Btn icon="plus" tooltip={locale('zoom_in')}
onClick={() => setScale(Math.max(1, scale - 1))} />
{focused.map(s => <Btn label={s} class="no-pointer" /> )}
<Btn icon="sync" tooltip={locale('generate_new_seed')}
onClick={() => setSeed(randomSeed())} />
</div>
<canvas ref={canvas} width="64" height="64"></canvas>
<div class="full-preview">
<InteractiveCanvas2D onSetup={onSetup} onResize={onResize} onDraw={onDraw} onHover={onHover} pixelSize={4} startScale={1/8} minScale={1/32} maxScale={1} />
</div>
</>
}
@@ -1,7 +1,7 @@
import * as deepslate19 from 'deepslate/worldgen'
import type { VersionId } from '../services/index.js'
import { checkVersion, fetchAllPresets, fetchPreset } from '../services/index.js'
import { BiMap, clamp, computeIfAbsentAsync, deepClone, deepEqual, isObject, square } from '../Utils.js'
import type { VersionId } from '../../services/index.js'
import { checkVersion, fetchAllPresets, fetchPreset } from '../../services/index.js'
import { clamp, computeIfAbsent, computeIfAbsentAsync, deepClone, deepEqual, isObject, square } from '../../Utils.js'
export type ProjectData = Record<string, Record<string, unknown>>
@@ -18,10 +18,10 @@ export class Deepslate {
private loadingPromise: Promise<void> | undefined
private readonly deepslateCache = new Map<VersionId, typeof deepslate19>()
private readonly Z = 0
private readonly DEBUG = false
private cacheState: unknown
private settingsCache: NoiseSettings | undefined
private routerCache: NoiseRouter | undefined
private generatorCache: ChunkGenerator | undefined
private biomeSourceCache: BiomeSource | undefined
private randomStateCache: deepslate19.RandomState | undefined
@@ -106,8 +106,11 @@ export class Deepslate {
this.generatorCache = chunkGenerator
if (this.isVersion('1.19')) {
this.randomStateCache = new this.d.RandomState(noiseSettings, seed)
this.routerCache = this.randomStateCache.router
} else {
this.randomStateCache = undefined
this.routerCache = undefined
}
this.biomeSourceCache = {
getBiome: (x, y, z) => biomeSource.getBiome(x, y, z, undefined!),
@@ -129,7 +132,8 @@ export class Deepslate {
biomeState = { type: biomeState.type, biomes }
}
if (this.isVersion('1.19')) {
return this.d.BiomeSource.fromJson(biomeState)
const bs = this.d.BiomeSource.fromJson(biomeState)
return bs
} else {
const root = isObject(biomeState) ? biomeState : {}
const type = typeof root.type === 'string' ? root.type.replace(/^minecraft:/, '') : undefined
@@ -242,61 +246,20 @@ export class Deepslate {
})
}
public fillBiomes(minX: number, maxX: number, minZ: number, maxZ: number, step = 1, y = 64) {
if (!this.generatorCache || !this.settingsCache) {
throw new Error('Tried to fill biomes before generator is loaded')
}
const quartY = (y - this.settingsCache.minY) >> 2
const minQuartX = minX >> 2
const maxQuartX = maxX >> 2
const minQuartZ = minZ >> 2
const maxQuartZ = maxZ >> 2
const countX = Math.floor((maxQuartX - minQuartX) / step)
const countZ = Math.floor((maxQuartZ - minQuartZ) / step)
const biomeIds = new BiMap<string, number>()
const data = new Int8Array(countX * countZ)
let biomeId = 0
let i = 0
for (let x = minQuartX; x < maxQuartX; x += step) {
for (let z = minQuartZ; z < maxQuartZ; z += step) {
const posKey = `${x}:${quartY}:${z}`
let biome = this.biomeCache.get(posKey)
if (!biome) {
if (this.DEBUG) {
biome = this.computeDebugBiome(x, z)
} else if (this.isVersion('1.19')) {
if (!this.randomStateCache) {
throw new Error('Tried to compute biomes before random state is loaded')
}
biome = this.generatorCache.computeBiome(this.randomStateCache, x, quartY, z).toString()
} else {
if(!this.biomeSourceCache) {
throw new Error('Tried to compute biomes before biome source is loaded')
}
biome = this.biomeSourceCache.getBiome(x, quartY, z).toString()
}
this.biomeCache.set(posKey, biome)
public getBiome(x: number, y: number, z: number) {
return computeIfAbsent(this.biomeCache, `${x}:${y}:${z}`, () => {
if (this.isVersion('1.19')) {
if (!this.randomStateCache || !this.generatorCache) {
throw new Error('Tried to compute biomes before random state is loaded')
}
data[i++] = biomeIds.computeIfAbsent(biome, () => biomeId++)
return this.generatorCache.computeBiome(this.randomStateCache, x, y, z).toString()
} else {
if(!this.biomeSourceCache) {
throw new Error('Tried to compute biomes before biome source is loaded')
}
return this.biomeSourceCache.getBiome(x, y, z).toString()
}
}
return {
palette: biomeIds.backward,
data,
width: countX,
height: countZ,
}
}
private computeDebugBiome(x: number, z: number) {
if (x > 0) {
return z > 0 ? 'minecraft:plains' : 'minecraft:forest'
} else {
return z > 0 ? 'minecraft:badlands' : 'minecraft:desert'
}
})
}
public loadDensityFunction(state: unknown, minY: number, height: number, seed: bigint) {
@@ -340,6 +303,13 @@ export class Deepslate {
return this.settingsCache
}
public getNoiseRouter(): NoiseRouter {
if (!this.routerCache) {
throw new Error('Tried to access noise router when they are not loaded')
}
return this.routerCache
}
public getBlockState(x: number, y: number) {
x = Math.floor(x)
y = Math.floor(y)
@@ -362,6 +332,15 @@ interface NoiseSettings {
height: number
}
interface NoiseRouter {
temperature: deepslate19.DensityFunction
vegetation: deepslate19.DensityFunction
continents: deepslate19.DensityFunction
erosion: deepslate19.DensityFunction
ridges: deepslate19.DensityFunction
depth: deepslate19.DensityFunction
}
interface ChunkGenerator {
fill(randomState: deepslate19.RandomState, chunk: Chunk, onlyFirstZ?: boolean): void
buildSurface(randomState: deepslate19.RandomState, chunk: Chunk, biome: string): void
@@ -1,77 +1,132 @@
import { useEffect, useRef, useState } from 'preact/hooks'
import { useLocale, useProject } from '../../contexts/index.js'
import { useCanvas } from '../../hooks/index.js'
import type { ColormapType } from '../../previews/Colormap.js'
import { densityFunction, densityPoint } from '../../previews/index.js'
import { DataModel } from '@mcschema/core'
import type { Voxel } from 'deepslate/render'
import { clampedMap, VoxelRenderer } from 'deepslate/render'
import type { mat3, mat4 } from 'gl-matrix'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { getProjectData, useLocale, useProject, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { Store } from '../../Store.js'
import { randomSeed } from '../../Utils.js'
import { Btn, BtnMenu } from '../index.js'
import { iterateWorld2D, randomSeed } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
import { ColormapSelector } from './ColormapSelector.jsx'
import { DEEPSLATE } from './Deepslate.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx'
export const DensityFunctionPreview = ({ data, shown, version }: PreviewProps) => {
export const DensityFunctionPreview = ({ data, shown }: PreviewProps) => {
const { locale } = useLocale()
const { project } = useProject()
const { version } = useVersion()
const [voxelMode, setVoxelMode] = useState(false)
const [seed, setSeed] = useState(randomSeed())
const [minY] = useState(0)
const [height] = useState(256)
const [autoScroll, setAutoScroll] = useState(false)
const serializedData = JSON.stringify(data)
const { value: df } = useAsync(async () => {
await DEEPSLATE.loadVersion(version, getProjectData(project))
const df = DEEPSLATE.loadDensityFunction(DataModel.unwrapLists(data), minY, height, seed)
return df
}, [version, project, minY, height, seed, serializedData])
// === 2D ===
const imageData = useRef<ImageData>()
const ctx = useRef<CanvasRenderingContext2D>()
const [focused, setFocused] = useState<string[]>([])
const [colormap, setColormap] = useState<ColormapType>(Store.getColormap() ?? 'viridis')
const offset = useRef(0)
const scrollInterval = useRef<number | undefined>(undefined)
const state = JSON.stringify([data])
const size = 256
const { canvas, redraw } = useCanvas({
size() {
return [size, size]
},
async draw(img) {
const options = { offset: offset.current, width: img.width, seed, version, project, minY, height, colormap }
await densityFunction(data, img, options)
},
async onDrag(dx) {
offset.current += dx * size
redraw()
},
async onHover(x, y) {
const worldX = Math.floor(x * size)
const worldY = Math.floor(y * (height - minY))
const options = { offset: offset.current, width: size, seed, version, project, minY, height, colormap }
const density = await densityPoint(data, worldX, worldY, options)
setFocused([density.toPrecision(3), `X=${Math.floor(worldX - offset.current)} Y=${(height - minY) - worldY}`])
},
onLeave() {
const onSetup2D = useCallback((canvas: HTMLCanvasElement) => {
const ctx2D = canvas.getContext('2d')
if (!ctx2D) return
ctx.current = ctx2D
}, [voxelMode])
const onResize2D = useCallback((width: number, height: number) => {
if (!ctx.current) return
imageData.current = ctx.current.getImageData(0, 0, width, height)
}, [voxelMode])
const onDraw2D = useCallback((transform: mat3) => {
if (!ctx.current || !imageData.current || !df) return
const colormapFn = getColormap(colormap)
const colorPicker = (t: number) => colormapFn(t <= 0.5 ? t - 0.08 : t + 0.08)
let limit = 0.01
iterateWorld2D(imageData.current, transform, (x, y) => {
const density = df.compute({ x, y, z: 0 })
limit = Math.max(limit, Math.min(1, Math.abs(density)))
return density
}, (density) => {
const color = colorPicker(clampedMap(density, -limit, limit, 1, 0))
return [color[0] * 256, color[1] * 256, color[2] * 256]
})
ctx.current.putImageData(imageData.current, 0, 0)
}, [voxelMode, df, colormap])
const onHover2D = useCallback((pos: [number, number] | undefined) => {
if (!pos || !df) {
setFocused([])
},
}, [version, state, seed, minY, height, colormap, project])
useEffect(() => {
if (scrollInterval.current) {
clearInterval(scrollInterval.current)
} else {
const [x, y] = pos
const output = df.compute({ x: x, y: -y, z: 0 })
setFocused([output.toPrecision(3), `X=${x} Y=${-y}`])
}
if (shown) {
redraw()
if (autoScroll) {
scrollInterval.current = setInterval(() => {
offset.current -= 8
redraw()
}, 100) as any
}, [voxelMode, df])
// === 3D ===
const renderer = useRef<VoxelRenderer | undefined>(undefined)
const [state, setState] = useState(0)
const [cutoff, setCutoff] = useState(0)
const onSetup3D = useCallback((canvas: HTMLCanvasElement) => {
const gl = canvas.getContext('webgl')
if (!gl) return
renderer.current = new VoxelRenderer(gl)
}, [voxelMode])
const onResize3D = useCallback((width: number, height: number) => {
renderer.current?.setViewport(0, 0, width, height)
}, [voxelMode])
const onDraw3D = useCallback((transform: mat4) => {
renderer.current?.draw(transform)
}, [voxelMode])
useEffect(() => {
if (!renderer.current || !shown || !df || !voxelMode) return
const voxels: Voxel[] = []
const maxY = minY + height
for (let x = 0; x < 16; x += 1) {
for (let y = minY; y < maxY; y += 1) {
for (let z = 0; z < 16; z += 1) {
const density = df.compute({ x, y, z })
if (density > cutoff) {
voxels.push({ x, y, z, color: [200, 200, 200] })
}
}
}
}
}, [version, state, seed, minY, height, colormap, project, shown, autoScroll])
renderer.current.setVoxels(voxels)
setState(state => state + 1)
}, [voxelMode, df, cutoff])
return <>
<div class="controls preview-controls">
{focused.map(s => <Btn label={s} class="no-pointer" /> )}
<ColormapSelector value={colormap} onChange={setColormap} />
<BtnMenu icon="gear" tooltip={locale('terrain_settings')}>
<Btn icon={autoScroll ? 'square_fill' : 'square'} label={locale('preview.auto_scroll')} onClick={() => setAutoScroll(!autoScroll)} />
</BtnMenu>
{voxelMode ? <>
<BtnMenu icon="gear">
<div class="btn btn-input" onClick={e => e.stopPropagation()}>
<span>{locale('cutoff')}</span>
<NumberInput value={cutoff} onChange={setCutoff} />
</div>
</BtnMenu>
</> : <>
<ColormapSelector value={colormap} onChange={setColormap} />
</>}
<Btn label={voxelMode ? locale('3d') : locale('2d')} onClick={() => setVoxelMode(!voxelMode)} />
<Btn icon="sync" tooltip={locale('generate_new_seed')}
onClick={() => setSeed(randomSeed())} />
</div>
<canvas ref={canvas} width={size} height={size}></canvas>
<div class="full-preview">{voxelMode
? <InteractiveCanvas3D onSetup={onSetup3D} onDraw={onDraw3D} onResize={onResize3D} state={state} startDistance={100} startPosition={[8, 120, 8]} />
: <InteractiveCanvas2D onSetup={onSetup2D} onDraw={onDraw2D} onHover={onHover2D} onResize={onResize2D} state={state} pixelSize={4} />
}</div>
</>
}
@@ -0,0 +1,127 @@
import { mat3, vec2 } from 'gl-matrix'
import { useCallback, useEffect, useRef } from 'preact/hooks'
interface Props {
onSetup: (canvas: HTMLCanvasElement) => void,
onDraw: (transform: mat3) => void,
onHover?: (offset: [number, number] | undefined) => void,
onResize: (width: number, height: number) => void,
state?: unknown,
pixelSize?: number,
startPosition?: [number, number],
startScale?: number,
minScale?: number,
maxScale?: number,
}
export function InteractiveCanvas2D({ onSetup, onDraw, onHover, onResize, state, pixelSize = 1, startPosition, startScale, minScale = 1/16, maxScale = 16 }: Props) {
const canvas = useRef<HTMLCanvasElement>(null)
const dragStart = useRef<[number, number] | undefined>()
const dragButton = useRef<number | undefined>()
const centerPos = useRef<[number, number]>(startPosition ? [-startPosition[0], -startPosition[1]] : [0, 0])
const viewScale = useRef(startScale ?? 1)
const frameRequest = useRef<number>()
// Transforms screen coordinates to world coordinates
const transform = useCallback(() => {
const mat = mat3.create()
if (!canvas.current) return mat
const halfWidth = Math.floor(canvas.current.clientWidth / 2) / pixelSize
const halfHeight = Math.floor(canvas.current.clientHeight / 2) / pixelSize
const scale = Math.pow(2, Math.floor(Math.log(viewScale.current * pixelSize) /Math.log(2)))
const offsetX = Math.floor(centerPos.current[0])
const offsetY = Math.floor(centerPos.current[1])
mat3.translate(mat, mat, [offsetX, offsetY])
mat3.scale(mat, mat, [scale, scale])
mat3.translate(mat, mat, [-halfWidth, -halfHeight])
return mat
}, [pixelSize])
const redraw = useRef<Function>(() => {})
useEffect(() => {
redraw.current = function requestRedraw() {
if (frameRequest.current !== undefined) {
cancelAnimationFrame(frameRequest.current)
}
frameRequest.current = requestAnimationFrame(function redraw() {
onDraw(transform())
})
}
}, [onDraw, transform])
useEffect(function changeDetected() {
redraw.current()
}, [state, onDraw, transform])
useEffect(function setupListeners() {
if (!canvas.current) return
function onMouseDown(e: MouseEvent) {
if (!canvas.current) return
dragStart.current = [e.offsetX, e.offsetY]
dragButton.current = e.button
}
function onMouseMove(e: MouseEvent) {
if (!canvas.current) return
if (dragStart.current === undefined) {
const pos = vec2.fromValues(e.offsetX / pixelSize, e.offsetY / pixelSize)
vec2.transformMat3(pos, pos, transform())
onHover?.([Math.floor(pos[0]), Math.floor(pos[1])])
} else {
const dragEnd: [number, number] = [e.offsetX, e.offsetY]
const dx = (dragEnd[0] - dragStart.current[0]) * (viewScale.current)
const dy = (dragEnd[1] - dragStart.current[1]) * (viewScale.current)
centerPos.current = [centerPos.current[0] - dx, centerPos.current[1] - dy]
dragStart.current = dragEnd
redraw.current()
}
}
function onMouseUp () {
dragStart.current = undefined
}
function onMouseLeave () {
onHover?.(undefined)
}
function onWheel (e: WheelEvent) {
const newScale = Math.pow(2, Math.log(viewScale.current) / Math.log(2) + e.deltaY / 200)
if (newScale > minScale && newScale < maxScale) {
viewScale.current = newScale
redraw.current()
}
e.preventDefault()
}
function onContextMenu(evt: MouseEvent) {
evt.preventDefault()
}
function resizeHandler() {
if (!canvas.current) return
const width = Math.floor(canvas.current.clientWidth / pixelSize)
const height = Math.floor(canvas.current.clientHeight / pixelSize)
canvas.current.width = width
canvas.current.height = height
onResize?.(width, height)
redraw.current()
}
onSetup(canvas.current)
resizeHandler()
canvas.current.addEventListener('mousedown', onMouseDown)
canvas.current.addEventListener('mousemove', onMouseMove)
canvas.current.addEventListener('mouseleave', onMouseLeave)
document.body.addEventListener('mouseup', onMouseUp)
canvas.current.addEventListener('wheel', onWheel)
canvas.current.addEventListener('contextmenu', onContextMenu)
window.addEventListener('resize', resizeHandler)
return () => {
canvas.current?.removeEventListener('mousedown', onMouseDown)
canvas.current?.removeEventListener('mousemove', onMouseMove)
canvas.current?.removeEventListener('mouseleave', onMouseLeave)
document.body.removeEventListener('mouseup', onMouseUp)
canvas.current?.removeEventListener('wheel', onWheel)
canvas.current?.removeEventListener('contextmenu', onContextMenu)
window.removeEventListener('resize', resizeHandler)
}
}, [onSetup, onResize, onHover, transform, pixelSize, minScale, maxScale])
return <canvas ref={canvas}></canvas>
}
@@ -0,0 +1,125 @@
import { mat4, vec3 } from 'gl-matrix'
import { useEffect, useRef } from 'preact/hooks'
interface Props {
onSetup: (canvas: HTMLCanvasElement) => void,
onDraw: (transform: mat4) => void,
onResize: (width: number, height: number) => void,
state?: unknown,
startPosition?: [number, number, number],
startDistance?: number,
startYRotation?: number,
startXRotation?: number,
}
export function InteractiveCanvas3D({ onSetup, onDraw, onResize, state, startPosition, startDistance, startYRotation, startXRotation }: Props) {
const canvas = useRef<HTMLCanvasElement>(null)
const dragStart = useRef<[number, number] | undefined>()
const dragButton = useRef<number | undefined>()
const centerPos = useRef<[number, number, number]>(startPosition ? [-startPosition[0], -startPosition[1], -startPosition[2]] : [0, 0, 0])
const viewDist = useRef(startDistance ?? 4)
const yRotation = useRef(startYRotation ?? 0.8)
const xRotation = useRef(startXRotation ?? 0.5)
const frameRequest = useRef<number>()
const redraw = useRef<Function>(() => {})
useEffect(() => {
redraw.current = function requestRedraw() {
if (frameRequest.current !== undefined) {
cancelAnimationFrame(frameRequest.current)
}
frameRequest.current = requestAnimationFrame(function redraw() {
yRotation.current = yRotation.current % (Math.PI * 2)
xRotation.current = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, xRotation.current))
viewDist.current = Math.max(1, viewDist.current)
const transform = mat4.create()
mat4.translate(transform, transform, [0, 0, -viewDist.current])
mat4.rotateX(transform, transform, xRotation.current)
mat4.rotateY(transform, transform, yRotation.current)
mat4.translate(transform, transform, centerPos.current)
onDraw(transform)
})
}
}, [onDraw])
useEffect(function changeDetected() {
redraw.current()
}, [state, onDraw])
useEffect(function setupListeners() {
if (!canvas.current) return
function onMouseDown(e: MouseEvent) {
dragStart.current = [e.offsetX, e.offsetY]
dragButton.current = e.button
}
function onMouseMove(e: MouseEvent) {
if (dragStart.current === undefined) {
return
}
const dx = e.offsetX - dragStart.current[0]
const dy = e.offsetY - dragStart.current[1]
if (dx === 0 && dy === 0) {
return
}
if (dragButton.current === 0) {
yRotation.current += dx / 100
xRotation.current += dy / 100
redraw.current()
} else if (dragButton.current === 1 || dragButton.current === 2) {
const pos = vec3.fromValues(centerPos.current[0], centerPos.current[1], centerPos.current[2])
vec3.rotateY(pos, pos, [0, 0, 0], yRotation.current)
vec3.rotateX(pos, pos, [0, 0, 0], xRotation.current)
const d = vec3.fromValues(dx / 100, -dy / 100, 0)
vec3.scale(d, d, 0.25 * viewDist.current)
vec3.add(pos, pos, d)
vec3.rotateX(pos, pos, [0, 0, 0], -xRotation.current)
vec3.rotateY(pos, pos, [0, 0, 0], -yRotation.current)
centerPos.current = [pos[0], pos[1], pos[2]]
redraw.current()
}
dragStart.current = [e.offsetX, e.offsetY]
}
function onMouseUp() {
dragStart.current = undefined
}
function onWheel(evt: WheelEvent) {
viewDist.current = Math.max(1, viewDist.current + evt.deltaY / 100)
redraw.current()
evt.preventDefault()
}
function onContextMenu(evt: MouseEvent) {
evt.preventDefault()
}
function resizeHandler() {
if (!canvas.current) return
const width = canvas.current.clientWidth
const height = canvas.current.clientHeight
canvas.current.width = width
canvas.current.height = height
onResize?.(width, height)
redraw.current()
}
console.warn('Uhhh...', canvas.current)
onSetup(canvas.current)
resizeHandler()
canvas.current.addEventListener('mousedown', onMouseDown)
canvas.current.addEventListener('mousemove', onMouseMove)
document.body.addEventListener('mouseup', onMouseUp)
canvas.current.addEventListener('wheel', onWheel)
canvas.current.addEventListener('contextmenu', onContextMenu)
window.addEventListener('resize', resizeHandler)
return () => {
canvas.current?.removeEventListener('mousedown', onMouseDown)
canvas.current?.removeEventListener('mousemove', onMouseMove)
document.body.removeEventListener('mouseup', onMouseUp)
canvas.current?.removeEventListener('wheel', onWheel)
canvas.current?.removeEventListener('contextmenu', onContextMenu)
window.removeEventListener('resize', resizeHandler)
}
}, [onSetup, onResize])
return <canvas ref={canvas}></canvas>
}
@@ -1,8 +1,8 @@
import type { Random } from 'deepslate/core'
import { Enchantment, Identifier, ItemStack, LegacyRandom } from 'deepslate/core'
import { NbtCompound, NbtInt, NbtList, NbtShort, NbtString, NbtTag, NbtType } from 'deepslate/nbt'
import type { VersionId } from '../services/Schemas.js'
import { clamp, getWeightedRandom, isObject } from '../Utils.js'
import type { VersionId } from '../../services/Schemas.js'
import { clamp, getWeightedRandom, isObject } from '../../Utils.js'
export interface SlottedItem {
slot: number,
@@ -1,12 +1,12 @@
import { DataModel } from '@mcschema/core'
import { useEffect, useRef, useState } from 'preact/hooks'
import { useLocale, useVersion } from '../../contexts/index.js'
import type { SlottedItem } from '../../previews/LootTable.js'
import { generateLootTable } from '../../previews/LootTable.js'
import { clamp, randomSeed } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import { ItemDisplay } from '../ItemDisplay.jsx'
import type { PreviewProps } from './index.js'
import type { SlottedItem } from './LootTable.js'
import { generateLootTable } from './LootTable.js'
export const LootTablePreview = ({ data }: PreviewProps) => {
const { locale } = useLocale()
+50 -53
View File
@@ -1,76 +1,73 @@
import { useEffect, useRef, useState } from 'preact/hooks'
import { DataModel } from '@mcschema/core'
import { clampedMap, NoiseParameters, NormalNoise, XoroshiroRandom } from 'deepslate'
import type { mat3 } from 'gl-matrix'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale } from '../../contexts/index.js'
import { useCanvas } from '../../hooks/index.js'
import type { ColormapType } from '../../previews/Colormap.js'
import { normalNoise, normalNoisePoint } from '../../previews/index.js'
import { Store } from '../../Store.js'
import { randomSeed } from '../../Utils.js'
import { iterateWorld2D, randomSeed } from '../../Utils.js'
import { Btn } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
import { ColormapSelector } from './ColormapSelector.jsx'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
export const NoisePreview = ({ data, shown, version }: PreviewProps) => {
export const NoisePreview = ({ data, shown }: PreviewProps) => {
const { locale } = useLocale()
const [seed, setSeed] = useState(randomSeed())
const [scale, setScale] = useState(2)
const state = JSON.stringify(data)
const noise = useMemo(() => {
const random = XoroshiroRandom.create(seed)
const params = NoiseParameters.fromJson(DataModel.unwrapLists(data))
return new NormalNoise(random, params)
}, [state, seed])
const imageData = useRef<ImageData>()
const ctx = useRef<CanvasRenderingContext2D>()
const [focused, setFocused] = useState<string[]>([])
const [colormap, setColormap] = useState<ColormapType>(Store.getColormap() ?? 'viridis')
const offset = useRef<[number, number]>([0, 0])
const state = JSON.stringify([data])
const { canvas, redraw } = useCanvas({
size() {
return [256, 256]
},
async draw(img) {
const options = { offset: offset.current, scale, seed, version, colormap }
normalNoise(data, img, options)
},
async onDrag(dx, dy) {
offset.current[0] = offset.current[0] + dx * 256
offset.current[1] = offset.current[1] + dy * 256
redraw()
},
onHover(x, y) {
const x2 = Math.floor(x * 256)
const y2 = Math.floor(y * 256)
const options = { offset: offset.current, scale, seed, version, colormap }
const value = normalNoisePoint(data, x2, y2, options)
const ox = -options.offset[0] - 100
const oy = -options.offset[1] - 100
const xx = (x2 + ox) * options.scale
const yy = (y2 + oy) * options.scale
setFocused([value.toPrecision(3), `X=${Math.floor(xx)} Y=${Math.floor(yy)}`])
},
onLeave() {
const onSetup = useCallback((canvas: HTMLCanvasElement) => {
const ctx2D = canvas.getContext('2d')
if (!ctx2D) return
ctx.current = ctx2D
}, [])
const onResize = useCallback((width: number, height: number) => {
if (!ctx.current) return
imageData.current = ctx.current.getImageData(0, 0, width, height)
}, [])
const onDraw = useCallback((transform: mat3) => {
if (!ctx.current || !imageData.current || !shown) return
const colorPicker = getColormap(colormap)
iterateWorld2D(imageData.current, transform, (x, y) => {
return noise.sample(x, y, 0)
}, (value) => {
const color = colorPicker(clampedMap(value, -1, 1, 1, 0))
return [color[0] * 256, color[1] * 256, color[2] * 256]
})
ctx.current.putImageData(imageData.current, 0, 0)
}, [noise, colormap, shown])
const onHover = useCallback((pos: [number, number] | undefined) => {
if (!pos) {
setFocused([])
},
}, [version, state, scale, seed, colormap])
useEffect(() => {
if (shown) {
redraw()
} else {
const [x, y] = pos
const output = noise.sample(x, -y, 0)
setFocused([output.toPrecision(3), `X=${x} Y=${-y}`])
}
}, [version, state, scale, seed, colormap, shown])
const changeScale = (newScale: number) => {
offset.current[0] = offset.current[0] * scale / newScale
offset.current[1] = offset.current[1] * scale / newScale
setScale(newScale)
}
}, [noise])
return <>
<div class="controls preview-controls">
{focused.map(s => <Btn label={s} class="no-pointer" /> )}
<ColormapSelector value={colormap} onChange={setColormap} />
<Btn icon="dash" tooltip={locale('zoom_out')}
onClick={() => changeScale(scale * 1.5)} />
<Btn icon="plus" tooltip={locale('zoom_in')}
onClick={() => changeScale(scale / 1.5)} />
<Btn icon="sync" tooltip={locale('generate_new_seed')}
onClick={() => setSeed(randomSeed())} />
</div>
<canvas ref={canvas} width="256" height="256"></canvas>
<div class="full-preview">
<InteractiveCanvas2D onSetup={onSetup} onResize={onResize} onDraw={onDraw} onHover={onHover} pixelSize={4} />
</div>
</>
}
@@ -1,79 +1,90 @@
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale, useProject } from '../../contexts/index.js'
import { useCanvas } from '../../hooks/index.js'
import type { ColormapType } from '../../previews/Colormap.js'
import { densityFunction, getNoiseBlock, noiseSettings } from '../../previews/index.js'
import { CachedCollections, checkVersion } from '../../services/index.js'
import { DataModel } from '@mcschema/core'
import { clampedMap } from 'deepslate'
import type { mat3 } from 'gl-matrix'
import { vec2 } from 'gl-matrix'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { getProjectData, useLocale, useProject } from '../../contexts/index.js'
import { useAsync } from '../../hooks/index.js'
import { CachedCollections } from '../../services/index.js'
import { Store } from '../../Store.js'
import { randomSeed } from '../../Utils.js'
import { iterateWorld2D, randomSeed } from '../../Utils.js'
import { Btn, BtnInput, BtnMenu } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
import { ColormapSelector } from './ColormapSelector.jsx'
import { DEEPSLATE } from './Deepslate.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
export const NoiseSettingsPreview = ({ data, shown, version }: PreviewProps) => {
const { locale } = useLocale()
const { project } = useProject()
const [seed, setSeed] = useState(randomSeed())
const [biome, setBiome] = useState('minecraft:plains')
const [biomeScale, setBiomeScale] = useState(0.2)
const [biomeDepth, setBiomeDepth] = useState(0.1)
const [autoScroll, setAutoScroll] = useState(false)
const [focused, setFocused] = useState<string[]>([])
const [layer, setLayer] = useState('terrain')
const state = JSON.stringify(data)
const { value } = useAsync(async () => {
const unwrapped = DataModel.unwrapLists(data)
await DEEPSLATE.loadVersion(version, getProjectData(project))
const biomeSource = { type: 'fixed', biome }
await DEEPSLATE.loadChunkGenerator(unwrapped, biomeSource, seed)
const noiseSettings = DEEPSLATE.getNoiseSettings()
const finalDensity = DEEPSLATE.loadDensityFunction(unwrapped?.noise_router?.final_density, noiseSettings.minY, noiseSettings.height, seed)
return { noiseSettings, finalDensity }
}, [state, seed, version, project, biome])
const { noiseSettings, finalDensity } = value ?? {}
const imageData = useRef<ImageData>()
const ctx = useRef<CanvasRenderingContext2D>()
const [focused, setFocused] = useState<string[]>([])
const [colormap, setColormap] = useState<ColormapType>(Store.getColormap() ?? 'viridis')
const offset = useRef(0)
const scrollInterval = useRef<number | undefined>(undefined)
const state = JSON.stringify([data, biomeScale, biomeDepth])
const size = data?.noise?.height ?? 256
const { canvas, redraw } = useCanvas({
size() {
return [size, size]
},
async draw(img) {
const options = { biome, biomeDepth, biomeScale, offset: offset.current, width: img.width, seed, version, project, colormap, minY: data?.noise?.min_y ?? 0, height: data?.noise?.height ?? 256, hardZero: true }
if (layer === 'final_density') {
const df = data?.noise_router?.final_density ?? 0
await densityFunction(df, img, options)
} else {
await noiseSettings(data, img, options)
}
},
async onDrag(dx) {
offset.current += dx * size
redraw()
},
async onHover(x, y) {
const worldX = Math.floor(x * size - offset.current)
const worldY = size - Math.max(1, Math.ceil(y * size)) + (data?.noise?.min_y ?? 0)
const block = getNoiseBlock(worldX, worldY)
setFocused([block ? `Y=${worldY} (${block.getName().path})` : `Y=${worldY}`])
},
onLeave() {
const onSetup = useCallback((canvas: HTMLCanvasElement) => {
const ctx2D = canvas.getContext('2d')
if (!ctx2D) return
ctx.current = ctx2D
}, [])
const onResize = useCallback((width: number, height: number) => {
if (!ctx.current) return
imageData.current = ctx.current.getImageData(0, 0, width, height)
}, [])
const onDraw = useCallback((transform: mat3) => {
if (!ctx.current || !imageData.current || !shown) return
if (layer === 'terrain') {
const pos = vec2.create()
const minX = vec2.transformMat3(pos, vec2.fromValues(0, 0), transform)[0]
const maxX = vec2.transformMat3(pos, vec2.fromValues(imageData.current.width-1, 0), transform)[0]
DEEPSLATE.generateChunks(minX, maxX - minX + 1)
iterateWorld2D(imageData.current, transform, (x, y) => {
return DEEPSLATE.getBlockState(x, y)?.getName().toString()
}, (block) => {
return BlockColors[block ?? 'minecraft:air']
})
} else if (layer === 'final_density') {
const colormapFn = getColormap(colormap)
const colorPicker = (t: number) => colormapFn(t <= 0.5 ? t - 0.08 : t + 0.08)
iterateWorld2D(imageData.current, transform, (x, y) => {
return finalDensity?.compute({ x, y, z: 0 }) ?? 0
}, (density) => {
const color = colorPicker(clampedMap(density, -1, 1, 1, 0))
return [color[0] * 256, color[1] * 256, color[2] * 256]
})
}
ctx.current.putImageData(imageData.current, 0, 0)
}, [noiseSettings, finalDensity, layer, colormap, shown])
const onHover = useCallback((pos: [number, number] | undefined) => {
if (!pos || !noiseSettings || !finalDensity) {
setFocused([])
},
}, [version, state, seed, project, shown, biome, biomeScale, biomeDepth, layer, colormap])
useEffect(() => {
if (scrollInterval.current) {
clearInterval(scrollInterval.current)
} else {
const [x, y] = pos
const inVoid = -y < noiseSettings.minY || -y >= noiseSettings.minY + noiseSettings.height
const density = finalDensity.compute({ x, y: -y, z: 0})
const block = inVoid ? 'void' : DEEPSLATE.getBlockState(x, -y)?.getName().path ?? 'unknown'
setFocused([`${block} D=${density.toPrecision(3)}`, `X=${x} Y=${-y}`])
}
if (shown) {
(async () => {
try {
await redraw()
if (autoScroll) {
scrollInterval.current = setInterval(() => {
offset.current -= 8
redraw()
}, 100) as any
}
} catch (e) {
throw e
}
})()
}
}, [version, state, seed, project, shown, biome, biomeScale, biomeDepth, autoScroll, layer, colormap])
}, [noiseSettings, finalDensity])
const allBiomes = useMemo(() => CachedCollections?.get('worldgen/biome') ?? [], [version])
@@ -82,18 +93,33 @@ export const NoiseSettingsPreview = ({ data, shown, version }: PreviewProps) =>
{focused.map(s => <Btn label={s} class="no-pointer" /> )}
{layer === 'final_density' && <ColormapSelector value={colormap} onChange={setColormap} />}
<BtnMenu icon="gear" tooltip={locale('terrain_settings')}>
{checkVersion(version, undefined, '1.17') ? <>
<BtnInput label={locale('preview.scale')} value={`${biomeScale}`} onChange={v => setBiomeScale(Number(v))} />
<BtnInput label={locale('preview.depth')} value={`${biomeDepth}`} onChange={v => setBiomeDepth(Number(v))} />
</> :
<BtnInput label={locale('preview.biome')} value={biome} onChange={setBiome} dataList={allBiomes} larger />
}
<Btn icon={autoScroll ? 'square_fill' : 'square'} label={locale('preview.auto_scroll')} onClick={() => setAutoScroll(!autoScroll)} />
<BtnInput label={locale('preview.biome')} value={biome} onChange={setBiome} dataList={allBiomes} larger />
<Btn icon={layer === 'final_density' ? 'square_fill' : 'square'} label={locale('preview.final_density')} onClick={() => setLayer(layer === 'final_density' ? 'terrain' : 'final_density')} />
</BtnMenu>
<Btn icon="sync" tooltip={locale('generate_new_seed')}
onClick={() => setSeed(randomSeed())} />
</div>
<canvas ref={canvas} width={size} height={size}></canvas>
<div class="full-preview">
<InteractiveCanvas2D onSetup={onSetup} onResize={onResize} onDraw={onDraw} onHover={onHover} pixelSize={4} startScale={0.5} startPosition={[0, 64]} />
</div>
</>
}
const BlockColors: Record<string, [number, number, number]> = {
'minecraft:air': [150, 160, 170],
'minecraft:water': [20, 80, 170],
'minecraft:lava': [200, 100, 0],
'minecraft:stone': [55, 55, 55],
'minecraft:deepslate': [34, 34, 36],
'minecraft:bedrock': [10, 10, 10],
'minecraft:grass_block': [47, 120, 23],
'minecraft:dirt': [64, 40, 8],
'minecraft:gravel': [70, 70, 70],
'minecraft:sand': [196, 180, 77],
'minecraft:sandstone': [148, 135, 52],
'minecraft:netherrack': [100, 40, 40],
'minecraft:crimson_nylium': [144, 22, 22],
'minecraft:warped_nylium': [28, 115, 113],
'minecraft:basalt': [73, 74, 85],
'minecraft:end_stone': [200, 200, 140],
}
+10
View File
@@ -180,3 +180,13 @@ export function disectFilePath(path: string) {
}
return undefined
}
export function getProjectData(project: Project) {
return Object.fromEntries(['worldgen/noise_settings', 'worldgen/noise', 'worldgen/density_function'].map(type => {
const resources = Object.fromEntries(
project.files.filter(file => file.type === type)
.map<[string, unknown]>(file => [file.id, file.data])
)
return [type, resources]
}))
}
-1
View File
@@ -1,7 +1,6 @@
export * from './useActiveTimout.js'
export * from './useAsync.js'
export * from './useAsyncFn.js'
export * from './useCanvas.js'
export * from './useFocus.js'
export * from './useHash.js'
export * from './useLocalStorage.js'
-99
View File
@@ -1,99 +0,0 @@
import type { Inputs } from 'preact/hooks'
import { useEffect, useRef } from 'preact/hooks'
type Vec2 = [number, number]
export function useCanvas({ size, draw, onDrag, onHover, onLeave }: {
size: () => Vec2,
draw: (img: ImageData) => Promise<unknown>,
onDrag?: (dx: number, dy: number) => Promise<unknown>,
onHover?: (x: number, y: number) => unknown,
onLeave?: () => unknown,
}, inputs?: Inputs) {
const canvas = useRef<HTMLCanvasElement>(null)
const dragStart = useRef<Vec2 | undefined>()
const dragRequest = useRef<number>()
const dragPending = useRef<Vec2>([0, 0])
const dragBusy = useRef(false)
useEffect(() => {
if (!canvas.current) return
const onMouseDown = (e: MouseEvent) => {
dragStart.current = [e.offsetX, e.offsetY]
}
const onMouseMove = (e: MouseEvent) => {
if (dragStart.current === undefined) {
if (!canvas.current) return
const x = e.offsetX / canvas.current.clientWidth
const y = e.offsetY / canvas.current.clientHeight
onHover?.(x, y)
return
}
if (!onDrag) return
const dx = e.offsetX - dragStart.current[0]
const dy = e.offsetY - dragStart.current[1]
if (!(dx === 0 && dy === 0)) {
dragPending.current = [dragPending.current[0] + dx, dragPending.current[1] + dy]
if (!dragBusy.current) {
if (dragRequest.current) {
cancelAnimationFrame(dragRequest.current)
}
dragRequest.current = requestAnimationFrame(async () => {
if (!canvas.current) return
dragBusy.current = true
const dx = dragPending.current[0] / canvas.current.clientWidth
const dy = dragPending.current[1] / canvas.current.clientHeight
dragPending.current = [0, 0]
await onDrag?.(dx, dy)
dragBusy.current = false
})
}
}
dragStart.current = [e.offsetX, e.offsetY]
}
const onMouseUp = () => {
dragStart.current = undefined
}
const onMouseLeave = () => {
onLeave?.()
}
canvas.current.addEventListener('mousedown', onMouseDown)
canvas.current.addEventListener('mousemove', onMouseMove)
canvas.current.addEventListener('mouseleave', onMouseLeave)
document.body.addEventListener('mouseup', onMouseUp)
return () => {
canvas.current?.removeEventListener('mousedown', onMouseDown)
canvas.current?.removeEventListener('mousemove', onMouseMove)
canvas.current?.removeEventListener('mouseleave', onMouseLeave)
document.body.removeEventListener('mouseup', onMouseUp)
}
}, [...inputs ?? [], canvas.current])
const redraw = useRef<() => Promise<unknown>>()
const redrawCount = useRef(0)
redraw.current = async () => {
if (!canvas.current) return
const ctx = canvas.current.getContext('2d')!
const s = size()
canvas.current.width = s[0]
canvas.current.height = s[1]
const img = ctx.getImageData(0, 0, s[0], s[1])
const ownCount = redrawCount.current += 1
try {
await draw(img)
} catch (e) {
throw e
}
if (ownCount === redrawCount.current) {
ctx.putImageData(img, 0, 0)
}
}
return {
canvas,
redraw: redraw.current,
}
}
-194
View File
@@ -1,194 +0,0 @@
import { DataModel } from '@mcschema/core'
import type { Project } from '../contexts/Project.jsx'
import type { VersionId } from '../services/index.js'
import { stringToColor } from '../Utils.js'
import { DEEPSLATE } from './Deepslate.js'
import { getProjectData } from './NoiseSettings.js'
type Triple = [number, number, number]
type BiomeColors = Record<string, Triple>
type BiomeSourceOptions = {
biomeColors: BiomeColors,
offset: [number, number],
scale: number,
res: number,
seed: bigint,
version: VersionId,
settings: unknown,
project: Project,
y: number,
}
export async function biomeMap(state: any, img: ImageData, options: BiomeSourceOptions) {
await DEEPSLATE.loadVersion(options.version, getProjectData(options.project))
await DEEPSLATE.loadChunkGenerator(DataModel.unwrapLists(options.settings), DataModel.unwrapLists(state), options.seed)
const quartStep = Math.max(1, Math.round(options.scale))
const quartWidth = 200 * quartStep
const centerX = Math.round(-options.offset[0] * options.scale)
const centerZ = Math.round(-options.offset[1] * options.scale)
const minX = Math.floor(centerX - quartWidth / 2)
const minZ = Math.floor(centerZ - quartWidth / 2)
const maxX = minX + quartWidth
const maxZ = minZ + quartWidth
const { palette, data, width, height } = DEEPSLATE.fillBiomes(minX * 4, maxX * 4, minZ * 4, maxZ * 4, quartStep * options.res, options.y)
let x = 0
let z = 0
for (let i = 0; i < data.length; i += 1) {
const biome = palette.get(data[i])
const color = getBiomeColor(biome ?? '', options.biomeColors)
const j = z * width + x
img.data[j * 4] = color[0]
img.data[j * 4 + 1] = color[1]
img.data[j * 4 + 2] = color[2]
img.data[j * 4 + 3] = 255
z += 1
if (z >= height) {
z = 0
x += 1
}
}
}
export async function getBiome(state: any, x: number, z: number, options: BiomeSourceOptions): Promise<{[k: string]: number | string} | undefined> {
await DEEPSLATE.loadVersion(options.version, getProjectData(options.project))
await DEEPSLATE.loadChunkGenerator(DataModel.unwrapLists(options.settings), DataModel.unwrapLists( state), options.seed)
const quartStep = Math.max(1, Math.round(options.scale))
const centerX = Math.round(-options.offset[0] * options.scale)
const centerZ = Math.round(-options.offset[1] * options.scale)
const xx = Math.floor(centerX + ((x - 100) * quartStep))
const zz = Math.floor(centerZ + ((z - 100) * quartStep))
const { palette, data } = DEEPSLATE.fillBiomes(xx * 4, xx * 4 + 4, zz * 4, zz * 4 + 4, 1, options.y)
const biome = palette.get(data[0])!
return {
biome,
}
}
function getBiomeColor(biome: string, biomeColors: BiomeColors): Triple {
if (!biome) {
return [128, 128, 128]
}
const color = biomeColors[biome] ?? VanillaColors[biome]
if (color === undefined) {
return stringToColor(biome)
}
return color
}
export const VanillaColors: Record<string, Triple> = {
'minecraft:badlands': [217,69,21],
'minecraft:badlands_plateau': [202,140,101],
'minecraft:bamboo_jungle': [118,142,20],
'minecraft:bamboo_jungle_hills': [59,71,10],
'minecraft:basalt_deltas': [64,54,54],
'minecraft:beach': [250,222,85],
'minecraft:birch_forest': [48,116,68],
'minecraft:birch_forest_hills': [31,95,50],
'minecraft:cold_ocean': [32,32,112],
'minecraft:crimson_forest': [221,8,8],
'minecraft:dark_forest': [64,81,26],
'minecraft:dark_forest_hills': [104,121,66],
'minecraft:deep_cold_ocean': [32,32,56],
'minecraft:deep_frozen_ocean': [64,64,144],
'minecraft:deep_lukewarm_ocean': [0,0,64],
'minecraft:deep_ocean': [0,0,48],
'minecraft:deep_warm_ocean': [0,0,80],
'minecraft:desert': [250,148,24],
'minecraft:desert_hills': [210,95,18],
'minecraft:desert_lakes': [255,188,64],
'minecraft:end_barrens': [39,30,61],
'minecraft:end_highlands': [232,244,178],
'minecraft:end_midlands': [194,187,136],
'minecraft:eroded_badlands': [255,109,61],
'minecraft:flower_forest': [45,142,73],
'minecraft:forest': [5,102,33],
'minecraft:frozen_ocean': [112,112,214],
'minecraft:frozen_river': [160,160,255],
'minecraft:giant_spruce_taiga': [129,142,121],
'minecraft:old_growth_spruce_taiga': [129,142,121],
'minecraft:giant_spruce_taiga_hills': [109,119,102],
'minecraft:giant_tree_taiga': [89,102,81],
'minecraft:old_growth_pine_taiga': [89,102,81],
'minecraft:giant_tree_taiga_hills': [69,79,62],
'minecraft:gravelly_hills': [136,136,136],
'minecraft:gravelly_mountains': [136,136,136],
'minecraft:windswept_gravelly_hills': [136,136,136],
'minecraft:ice_spikes': [180,220,220],
'minecraft:jungle': [83,123,9],
'minecraft:jungle_edge': [98,139,23],
'minecraft:sparse_jungle': [98,139,23],
'minecraft:jungle_hills': [44,66,5],
'minecraft:lukewarm_ocean': [0,0,144],
'minecraft:modified_badlands_plateau': [242,180,141],
'minecraft:modified_gravelly_mountains': [120,152,120],
'minecraft:modified_jungle': [123,163,49],
'minecraft:modified_jungle_edge': [138,179,63],
'minecraft:modified_wooded_badlands_plateau': [216,191,141],
'minecraft:mountain_edge': [114,120,154],
'minecraft:extreme_hills': [96,96,96],
'minecraft:mountains': [96,96,96],
'minecraft:windswept_hills': [96,96,96],
'minecraft:mushroom_field_shore': [160,0,255],
'minecraft:mushroom_fields': [255,0,255],
'minecraft:nether_wastes': [191,59,59],
'minecraft:ocean': [0,0,112],
'minecraft:plains': [141,179,96],
'minecraft:river': [0,0,255],
'minecraft:savanna': [189,178,95],
'minecraft:savanna_plateau': [167,157,100],
'minecraft:shattered_savanna': [229,218,135],
'minecraft:windswept_savanna': [229,218,135],
'minecraft:shattered_savanna_plateau': [207,197,140],
'minecraft:small_end_islands': [16,12,28],
'minecraft:snowy_beach': [250,240,192],
'minecraft:snowy_mountains': [160,160,160],
'minecraft:snowy_taiga': [49,85,74],
'minecraft:snowy_taiga_hills': [36,63,54],
'minecraft:snowy_taiga_mountains': [89,125,114],
'minecraft:snowy_tundra': [255,255,255],
'minecraft:snowy_plains': [255,255,255],
'minecraft:soul_sand_valley': [94,56,48],
'minecraft:stone_shore': [162,162,132],
'minecraft:stony_shore': [162,162,132],
'minecraft:sunflower_plains': [181,219,136],
'minecraft:swamp': [7,249,178],
'minecraft:swamp_hills': [47,255,218],
'minecraft:taiga': [11,102,89],
'minecraft:taiga_hills': [22,57,51],
'minecraft:taiga_mountains': [51,142,129],
'minecraft:tall_birch_forest': [88,156,108],
'minecraft:old_growth_birch_forest': [88,156,108],
'minecraft:tall_birch_hills': [71,135,90],
'minecraft:the_end': [59,39,84],
'minecraft:the_void': [0,0,0],
'minecraft:warm_ocean': [0,0,172],
'minecraft:warped_forest': [73,144,123],
'minecraft:wooded_badlands_plateau': [176,151,101],
'minecraft:wooded_badlands': [176,151,101],
'minecraft:wooded_hills': [34,85,28],
'minecraft:wooded_mountains': [80,112,80],
'minecraft:windswept_forest': [80,112,80],
'minecraft:snowy_slopes': [140, 195, 222],
'minecraft:lofty_peaks': [196, 168, 193],
'minecraft:jagged_peaks': [196, 168, 193],
'minecraft:snowcapped_peaks': [200, 198, 200],
'minecraft:frozen_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],
'minecraft:deep_dark': [10, 14, 19],
'minecraft:mangrove_swamp': [36,196,142],
}
-144
View File
@@ -1,144 +0,0 @@
import { DataModel } from '@mcschema/core'
import { BlockState, clampedMap, DensityFunction } from 'deepslate/worldgen'
import type { Project } from '../contexts/Project.jsx'
import type { VersionId } from '../services/index.js'
import { checkVersion } from '../services/index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
import { DEEPSLATE } from './Deepslate.js'
import { NoiseChunkGenerator as OldNoiseChunkGenerator } from './noise/NoiseChunkGenerator.js'
export type NoiseSettingsOptions = {
biome?: string,
biomeScale?: number,
biomeDepth?: number,
offset: number,
width: number,
seed: bigint,
version: VersionId,
project: Project,
minY?: number,
height?: number,
colormap?: ColormapType,
hardZero?: boolean,
}
const colors: Record<string, [number, number, number]> = {
'minecraft:air': [150, 160, 170],
'minecraft:water': [20, 80, 170],
'minecraft:lava': [200, 100, 0],
'minecraft:stone': [55, 55, 55],
'minecraft:deepslate': [34, 34, 36],
'minecraft:bedrock': [10, 10, 10],
'minecraft:grass_block': [47, 120, 23],
'minecraft:dirt': [64, 40, 8],
'minecraft:gravel': [70, 70, 70],
'minecraft:sand': [196, 180, 77],
'minecraft:sandstone': [148, 135, 52],
'minecraft:netherrack': [100, 40, 40],
'minecraft:crimson_nylium': [144, 22, 22],
'minecraft:warped_nylium': [28, 115, 113],
'minecraft:basalt': [73, 74, 85],
'minecraft:end_stone': [200, 200, 140],
}
export async function noiseSettings(state: any, img: ImageData, options: NoiseSettingsOptions) {
if (checkVersion(options.version, '1.18')) {
await DEEPSLATE.loadVersion(options.version, getProjectData(options.project))
const biomeSource = { type: 'fixed', biome: options.biome }
await DEEPSLATE.loadChunkGenerator(DataModel.unwrapLists(state), biomeSource, options.seed)
DEEPSLATE.generateChunks(-options.offset, options.width)
const noise = DEEPSLATE.getNoiseSettings()
const data = img.data
for (let x = 0; x < options.width; x += 1) {
for (let y = 0; y < noise.height; y += 1) {
const i = x * 4 + (noise.height-y-1) * 4 * img.width
const state = DEEPSLATE.getBlockState(x - options.offset, y + noise.minY) ?? BlockState.AIR
const color = colors[state.getName().toString()] ?? [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.biomeDepth ?? 0, options.biomeScale ?? 0, options.offset, options.width)
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.noise.height; y += 1) {
const i = y * row + x * 4
const color = getColor(noise, y)
data[i] = color
data[i + 1] = color
data[i + 2] = color
data[i + 3] = 255
}
}
}
export function getNoiseBlock(x: number, y: number) {
return DEEPSLATE.getBlockState(x, y)
}
export async function densityFunction(state: any, img: ImageData, options: NoiseSettingsOptions) {
await DEEPSLATE.loadVersion(options.version, getProjectData(options.project))
const fn = DEEPSLATE.loadDensityFunction(DataModel.unwrapLists(state), options.minY ?? 0, options.height ?? 256, options.seed)
const noise = DEEPSLATE.getNoiseSettings()
const arr = Array(options.width * noise.height)
let limit = 0.01
for (let x = 0; x < options.width; x += 1) {
for (let y = 0; y < noise.height; y += 1) {
const i = x + y * options.width
const density = fn.compute(DensityFunction.context(x - options.offset, noise.height - y - 1 + noise.minY, 0))
limit = Math.max(limit, Math.min(1, Math.abs(density)))
arr[i] = density
}
}
const colormap = getColormap(options.colormap ?? 'viridis')
const colorPicker = options.hardZero ? (t: number) => colormap(t <= 0.5 ? t - 0.08 : t + 0.08) : colormap
const min = -limit
const max = limit
const data = img.data
for (let i = 0; i < options.width * noise.height; i += 1) {
const color = colorPicker(clampedMap(arr[i], min, max, 1, 0))
data[4 * i] = color[0] * 256
data[4 * i + 1] = color[1] * 256
data[4 * i + 2] = color[2] * 256
data[4 * i + 3] = 255
}
}
export async function densityPoint(state: any, x: number, y: number, options: NoiseSettingsOptions) {
await DEEPSLATE.loadVersion(options.version, getProjectData(options.project))
const fn = DEEPSLATE.loadDensityFunction(DataModel.unwrapLists(state), options.minY ?? 0, options.height ?? 256, options.seed)
return fn.compute(DensityFunction.context(Math.floor(x - options.offset), (options.height ?? 256) - y, 0))
}
export function getProjectData(project: Project) {
return Object.fromEntries(['worldgen/noise_settings', 'worldgen/noise', 'worldgen/density_function'].map(type => {
const resources = Object.fromEntries(
project.files.filter(file => file.type === type)
.map<[string, unknown]>(file => [file.id, file.data])
)
return [type, resources]
}))
}
function getColor(noise: number[], y: number): number {
if (noise[y] > 0) {
return 0
}
if (noise[y+1] > 0) {
return 150
}
return 255
}
-49
View File
@@ -1,49 +0,0 @@
import { DataModel } from '@mcschema/core'
import { clampedMap, NoiseParameters, NormalNoise, XoroshiroRandom } from 'deepslate/worldgen'
import type { VersionId } from '../services/index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
export type NoiseOptions = {
offset: [number, number],
scale: number,
seed: bigint,
version: VersionId,
colormap: ColormapType,
}
export function normalNoise(state: any, img: ImageData, options: NoiseOptions) {
const random = XoroshiroRandom.create(options.seed)
const params = NoiseParameters.fromJson(DataModel.unwrapLists(state))
const noise = new NormalNoise(random, params)
const colormap = getColormap(options.colormap)
const ox = -options.offset[0] - 100
const oy = -options.offset[1] - 100
const data = img.data
for (let x = 0; x < 256; x += 1) {
for (let y = 0; y < 256; y += 1) {
const i = x * 4 + y * 4 * 256
const xx = (x + ox) * options.scale
const yy = (y + oy) * options.scale
const output = noise.sample(xx, yy, 0)
const color = colormap(clampedMap(output, -1, 1, 0, 1))
data[i] = color[0] * 256
data[i + 1] = color[1] * 256
data[i + 2] = color[2] * 256
data[i + 3] = 255
}
}
}
export function normalNoisePoint(state: any, x: number, y: number, options: NoiseOptions) {
const random = XoroshiroRandom.create(options.seed)
const params = NoiseParameters.fromJson(DataModel.unwrapLists(state))
const noise = new NormalNoise(random, params)
const ox = -options.offset[0] - 100
const oy = -options.offset[1] - 100
const xx = (x + ox) * options.scale
const yy = (y + oy) * options.scale
return noise.sample(xx, yy, 0)
}
-4
View File
@@ -1,4 +0,0 @@
export * from './BiomeSource.js'
export * from './Decorator.js'
export * from './NoiseSettings.js'
export * from './NormalNoise.js'
@@ -1,150 +0,0 @@
import { LegacyRandom, PerlinNoise } from 'deepslate/worldgen'
import { clampedLerp, lerp2 } from '../../Utils.js'
export class NoiseChunkGenerator {
private readonly minLimitPerlinNoise: PerlinNoise
private readonly maxLimitPerlinNoise: PerlinNoise
private readonly mainPerlinNoise: PerlinNoise
private readonly depthNoise: PerlinNoise
private settings: any = {}
private chunkWidth: number = 4
private chunkHeight: number = 4
private chunkCountY: number = 32
private biomeDepth: number = 0.1
private biomeScale: number = 0.2
private noiseColumnCache: (number[] | null)[] = []
private xOffset: number = 0
constructor(seed: bigint) {
const random = new LegacyRandom(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) {
this.settings = settings
this.chunkWidth = settings.size_horizontal * 4
this.chunkHeight = settings.size_vertical * 4
this.chunkCountY = Math.floor(settings.height / this.chunkHeight)
if (settings.amplified && depth > 0) {
depth = 1 + depth * 2
scale = 1 + scale * 4
}
this.biomeDepth = 0.265625 * (depth * 0.5 - 0.125)
this.biomeScale = 96.0 / (scale * 0.9 + 0.1)
this.noiseColumnCache = Array(width).fill(null)
this.xOffset = xOffset
}
public iterateNoiseColumn(x: number): number[] {
const data = Array(this.chunkCountY * this.chunkHeight)
const cx = Math.floor(x / this.chunkWidth)
const ox = Math.floor(x % this.chunkWidth) / this.chunkWidth
const noise1 = this.fillNoiseColumn(cx)
const noise2 = this.fillNoiseColumn(cx + 1)
for (let y = this.chunkCountY - 1; y >= 0; y -= 1) {
for (let yy = this.chunkHeight; yy >= 0; yy -= 1) {
const oy = yy / this.chunkHeight
const i = y * this.chunkHeight + yy
data[i] = lerp2(oy, ox, noise1[y], noise1[y+1], noise2[y], noise2[y+1])
}
}
return data
}
private fillNoiseColumn(x: number): number[] {
const cachedColumn = this.noiseColumnCache[x - this.xOffset]
if (cachedColumn) return cachedColumn
const data = Array(this.chunkCountY + 1)
const xzScale = 684.412 * this.settings.sampling.xz_scale
const yScale = 684.412 * this.settings.sampling.y_scale
const xzFactor = xzScale / this.settings.sampling.xz_factor
const yFactor = yScale / this.settings.sampling.y_factor
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)
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
noise += falloff * (falloff > 0 ? 4 : 1)
if (this.settings.top_slide.size > 0) {
noise = clampedLerp(
this.settings.top_slide.target,
noise,
(this.chunkCountY - y - (this.settings.top_slide.offset)) / (this.settings.top_slide.size)
)
}
if (this.settings.bottom_slide.size > 0) {
noise = clampedLerp(
this.settings.bottom_slide.target,
noise,
(y - (this.settings.bottom_slide.offset)) / (this.settings.bottom_slide.size)
)
}
data[y] = noise
}
this.noiseColumnCache[x - this.xOffset] = data
return data
}
private getRandomDensity(x: number): number {
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
}
private sampleAndClampNoise(x: number, y: number, z: number, xzScale: number, yScale: number, xzFactor: number, yFactor: number): number {
let a = 0
let b = 0
let c = 0
let d = 1
for (let i = 0; i < 16; i += 1) {
const x2 = PerlinNoise.wrap(x * xzScale * d)
const y2 = PerlinNoise.wrap(y * yScale * d)
const z2 = PerlinNoise.wrap(z * xzScale * d)
const e = yScale * d
const minLimitNoise = this.minLimitPerlinNoise.getOctaveNoise(i)
if (minLimitNoise) {
a += minLimitNoise.sample(x2, y2, z2, e, y * e) / d
}
const maxLimitNoise = this.maxLimitPerlinNoise.getOctaveNoise(i)
if (maxLimitNoise) {
b += maxLimitNoise.sample(x2, y2, z2, e, y * e) / d
}
if (i < 8) {
const mainNoise = this.mainPerlinNoise.getOctaveNoise(i)
if (mainNoise) {
c += mainNoise.sample(
PerlinNoise.wrap(x * xzFactor * d),
PerlinNoise.wrap(y * yFactor * d),
PerlinNoise.wrap(z * xzFactor * d),
yFactor * d,
y * yFactor * d
) / d
}
}
d /= 2
}
return clampedLerp(a / 512, b / 512, (c / 10 + 1) / 2)
}
}
+1 -1
View File
@@ -6,10 +6,10 @@ import { memo } from 'preact/compat'
import { useState } from 'preact/hooks'
import { Btn, Octicon } from '../components/index.js'
import { ItemDisplay } from '../components/ItemDisplay.jsx'
import { VanillaColors } from '../components/previews/BiomeSourcePreview.jsx'
import config from '../Config.js'
import { localize, useLocale, useStore } from '../contexts/index.js'
import { useFocus } from '../hooks/index.js'
import { VanillaColors } from '../previews/index.js'
import type { BlockStateRegistry, VersionId } from '../services/index.js'
import { CachedDecorator, CachedFeature } from '../services/index.js'
import { deepClone, deepEqual, generateUUID, hexId, hexToRgb, isObject, newSeed, rgbToHex, stringToColor } from '../Utils.js'
+8 -6
View File
@@ -1,4 +1,6 @@
{
"2d": "2D",
"3d": "3D",
"add": "Add",
"add_bottom": "Add to bottom",
"add_top": "Add to top",
@@ -22,6 +24,7 @@
"copy_share": "Copy share link",
"copied": "Copied!",
"copy_context": "Copy context",
"cutoff": "Cutoff",
"developed_by": "Developed by",
"dimension_type": "Dimension Type",
"dimension": "Dimension",
@@ -66,15 +69,14 @@
"indentation.tabs": "Tabs",
"item_modifier": "Item Modifier",
"language": "Language",
"layer": "Layer",
"layer.biomes": "Biomes",
"layer.temperature": "Temperature",
"layer.humidity": "Humidity",
"layer.continentalness": "Continentalness",
"layer.vegetation": "Humidity",
"layer.continents": "Continentalness",
"layer.erosion": "Erosion",
"layer.weirdness": "Weirdness",
"layer.offset": "Offset",
"layer.factor": "Factor",
"layer.jaggedness": "Jaggedness",
"layer.ridges": "Weirdness",
"layer.depth": "Depth",
"highlighting": "Highlighting",
"loading": "Loading...",
"loot_table": "Loot Table",
+28
View File
@@ -326,6 +326,11 @@ main > .controls {
margin-right: 8px;
}
.preview-controls,
.secondary-controls {
z-index: 1;
}
.secondary-controls {
margin-top: 40px;
}
@@ -414,6 +419,7 @@ main > .controls {
outline: none;
resize: none;
position: static;
opacity: 0.9;
background-color: var(--background-2);
border-top-left-radius: 6px;
color: var(--text-1);
@@ -463,12 +469,34 @@ main.has-preview {
image-rendering: -webkit-crisp-edges;
image-rendering: crisp-edges;
image-rendering: pixelated;
}
.popup-preview canvas,
.popup-preview .pixelated {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.full-preview,
.full-preview > canvas {
width: 100%;
height: 100%;
}
canvas.preview-details {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
visibility: hidden;
}
canvas.preview-details.visible {
visibility: visible;
}
.popup-share {
position: fixed;
display: flex;