mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-25 08:06:51 +00:00
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:
@@ -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],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user