refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
24
packages/framework/procgen/module.json
Normal file
24
packages/framework/procgen/module.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "procgen",
|
||||
"name": "@esengine/procgen",
|
||||
"globalKey": "procgen",
|
||||
"displayName": "Procedural Generation",
|
||||
"description": "程序化生成工具,包含噪声函数和随机工具 | Procedural generation tools with noise functions and random utilities",
|
||||
"version": "1.0.0",
|
||||
"category": "Math",
|
||||
"icon": "Wand",
|
||||
"tags": ["noise", "random", "procedural", "generation"],
|
||||
"isCore": false,
|
||||
"defaultEnabled": true,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": false,
|
||||
"platforms": ["web", "desktop"],
|
||||
"dependencies": ["core"],
|
||||
"exports": {
|
||||
"components": [],
|
||||
"systems": []
|
||||
},
|
||||
"requiresWasm": false,
|
||||
"outputPath": "dist/index.js",
|
||||
"pluginExport": "ProcGenPlugin"
|
||||
}
|
||||
40
packages/framework/procgen/package.json
Normal file
40
packages/framework/procgen/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@esengine/procgen",
|
||||
"version": "1.0.0",
|
||||
"description": "Procedural generation tools for ECS Framework / ECS 框架的程序化生成工具",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"module.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/blueprint": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"@types/node": "^20.19.17",
|
||||
"rimraf": "^5.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
82
packages/framework/procgen/src/index.ts
Normal file
82
packages/framework/procgen/src/index.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @esengine/procgen
|
||||
*
|
||||
* @zh 程序化生成工具包
|
||||
* @en Procedural Generation Toolkit
|
||||
*
|
||||
* @zh 提供噪声函数、随机工具和蓝图节点
|
||||
* @en Provides noise functions, random utilities, and blueprint nodes
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Noise Functions | 噪声函数
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
PerlinNoise,
|
||||
createPerlinNoise,
|
||||
SimplexNoise,
|
||||
createSimplexNoise,
|
||||
WorleyNoise,
|
||||
createWorleyNoise,
|
||||
FBM,
|
||||
createFBM
|
||||
} from './noise';
|
||||
|
||||
export type {
|
||||
DistanceFunction,
|
||||
INoise2D,
|
||||
INoise3D,
|
||||
FBMConfig
|
||||
} from './noise';
|
||||
|
||||
// =============================================================================
|
||||
// Random Utilities | 随机工具
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
SeededRandom,
|
||||
createSeededRandom,
|
||||
WeightedRandom,
|
||||
weightedPick,
|
||||
weightedPickFromMap,
|
||||
createWeightedRandom,
|
||||
shuffle,
|
||||
shuffleCopy,
|
||||
pickOne,
|
||||
sample,
|
||||
sampleWithReplacement,
|
||||
randomIntegers,
|
||||
weightedSample
|
||||
} from './random';
|
||||
|
||||
export type { WeightedItem } from './random';
|
||||
|
||||
// =============================================================================
|
||||
// Blueprint Nodes | 蓝图节点
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
// Templates
|
||||
SampleNoise2DTemplate,
|
||||
SampleFBMTemplate,
|
||||
SeededRandomTemplate,
|
||||
SeededRandomIntTemplate,
|
||||
WeightedPickTemplate,
|
||||
ShuffleArrayTemplate,
|
||||
PickRandomTemplate,
|
||||
SampleArrayTemplate,
|
||||
RandomPointInCircleTemplate,
|
||||
// Executors
|
||||
SampleNoise2DExecutor,
|
||||
SampleFBMExecutor,
|
||||
SeededRandomExecutor,
|
||||
SeededRandomIntExecutor,
|
||||
WeightedPickExecutor,
|
||||
ShuffleArrayExecutor,
|
||||
PickRandomExecutor,
|
||||
SampleArrayExecutor,
|
||||
RandomPointInCircleExecutor,
|
||||
// Collection
|
||||
ProcGenNodeDefinitions
|
||||
} from './nodes';
|
||||
475
packages/framework/procgen/src/nodes/ProcGenNodes.ts
Normal file
475
packages/framework/procgen/src/nodes/ProcGenNodes.ts
Normal file
@@ -0,0 +1,475 @@
|
||||
/**
|
||||
* @zh 程序化生成蓝图节点
|
||||
* @en Procedural Generation Blueprint Nodes
|
||||
*/
|
||||
|
||||
import type { BlueprintNodeTemplate, BlueprintNode, INodeExecutor, ExecutionResult } from '@esengine/blueprint';
|
||||
import { PerlinNoise } from '../noise/PerlinNoise';
|
||||
import { SimplexNoise } from '../noise/SimplexNoise';
|
||||
import { WorleyNoise } from '../noise/WorleyNoise';
|
||||
import { FBM } from '../noise/FBM';
|
||||
import { SeededRandom } from '../random/SeededRandom';
|
||||
import { weightedPick } from '../random/WeightedRandom';
|
||||
import type { WeightedItem } from '../random/WeightedRandom';
|
||||
import { shuffle, pickOne, sample } from '../random/Shuffle';
|
||||
|
||||
// =============================================================================
|
||||
// 噪声缓存 | Noise Cache
|
||||
// =============================================================================
|
||||
|
||||
const noiseCache = new Map<string, PerlinNoise | SimplexNoise | WorleyNoise>();
|
||||
const rngCache = new Map<number, SeededRandom>();
|
||||
|
||||
function getPerlinNoise(seed: number): PerlinNoise {
|
||||
const key = `perlin_${seed}`;
|
||||
if (!noiseCache.has(key)) {
|
||||
noiseCache.set(key, new PerlinNoise(seed));
|
||||
}
|
||||
return noiseCache.get(key) as PerlinNoise;
|
||||
}
|
||||
|
||||
function getSimplexNoise(seed: number): SimplexNoise {
|
||||
const key = `simplex_${seed}`;
|
||||
if (!noiseCache.has(key)) {
|
||||
noiseCache.set(key, new SimplexNoise(seed));
|
||||
}
|
||||
return noiseCache.get(key) as SimplexNoise;
|
||||
}
|
||||
|
||||
function getWorleyNoise(seed: number): WorleyNoise {
|
||||
const key = `worley_${seed}`;
|
||||
if (!noiseCache.has(key)) {
|
||||
noiseCache.set(key, new WorleyNoise(seed));
|
||||
}
|
||||
return noiseCache.get(key) as WorleyNoise;
|
||||
}
|
||||
|
||||
function getSeededRandom(seed: number): SeededRandom {
|
||||
if (!rngCache.has(seed)) {
|
||||
rngCache.set(seed, new SeededRandom(seed));
|
||||
}
|
||||
return rngCache.get(seed)!;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 执行上下文接口 | Execution Context Interface
|
||||
// =============================================================================
|
||||
|
||||
interface ProcGenContext {
|
||||
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown;
|
||||
setOutputs(nodeId: string, outputs: Record<string, unknown>): void;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SampleNoise2D 节点 | SampleNoise2D Node
|
||||
// =============================================================================
|
||||
|
||||
export const SampleNoise2DTemplate: BlueprintNodeTemplate = {
|
||||
type: 'SampleNoise2D',
|
||||
title: 'Sample Noise 2D',
|
||||
category: 'math',
|
||||
description: 'Sample 2D noise at coordinates / 在坐标处采样 2D 噪声',
|
||||
keywords: ['noise', 'perlin', 'simplex', 'random', 'procedural'],
|
||||
menuPath: ['Procedural', 'Noise', 'Sample Noise 2D'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'x', displayName: 'X', type: 'float' },
|
||||
{ name: 'y', displayName: 'Y', type: 'float' },
|
||||
{ name: 'seed', displayName: 'Seed', type: 'int' },
|
||||
{ name: 'noiseType', displayName: 'Type', type: 'string' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'float' }
|
||||
],
|
||||
color: '#9c27b0'
|
||||
};
|
||||
|
||||
export class SampleNoise2DExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ProcGenContext;
|
||||
const x = ctx.evaluateInput(node.id, 'x', 0) as number;
|
||||
const y = ctx.evaluateInput(node.id, 'y', 0) as number;
|
||||
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
|
||||
const noiseType = ctx.evaluateInput(node.id, 'noiseType', 'perlin') as string;
|
||||
|
||||
let value = 0;
|
||||
switch (noiseType.toLowerCase()) {
|
||||
case 'simplex':
|
||||
value = getSimplexNoise(seed).noise2D(x, y);
|
||||
break;
|
||||
case 'worley':
|
||||
value = getWorleyNoise(seed).noise2D(x, y);
|
||||
break;
|
||||
case 'perlin':
|
||||
default:
|
||||
value = getPerlinNoise(seed).noise2D(x, y);
|
||||
break;
|
||||
}
|
||||
|
||||
return { outputs: { value } };
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SampleFBM 节点 | SampleFBM Node
|
||||
// =============================================================================
|
||||
|
||||
export const SampleFBMTemplate: BlueprintNodeTemplate = {
|
||||
type: 'SampleFBM',
|
||||
title: 'Sample FBM',
|
||||
category: 'math',
|
||||
description: 'Sample Fractal Brownian Motion noise / 采样分形布朗运动噪声',
|
||||
keywords: ['noise', 'fbm', 'fractal', 'octave', 'terrain'],
|
||||
menuPath: ['Procedural', 'Noise', 'Sample FBM'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'x', displayName: 'X', type: 'float' },
|
||||
{ name: 'y', displayName: 'Y', type: 'float' },
|
||||
{ name: 'seed', displayName: 'Seed', type: 'int' },
|
||||
{ name: 'octaves', displayName: 'Octaves', type: 'int' },
|
||||
{ name: 'frequency', displayName: 'Frequency', type: 'float' },
|
||||
{ name: 'persistence', displayName: 'Persistence', type: 'float' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'float' }
|
||||
],
|
||||
color: '#9c27b0'
|
||||
};
|
||||
|
||||
export class SampleFBMExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ProcGenContext;
|
||||
const x = ctx.evaluateInput(node.id, 'x', 0) as number;
|
||||
const y = ctx.evaluateInput(node.id, 'y', 0) as number;
|
||||
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
|
||||
const octaves = ctx.evaluateInput(node.id, 'octaves', 6) as number;
|
||||
const frequency = ctx.evaluateInput(node.id, 'frequency', 1) as number;
|
||||
const persistence = ctx.evaluateInput(node.id, 'persistence', 0.5) as number;
|
||||
|
||||
const noise = getPerlinNoise(seed);
|
||||
const fbm = new FBM(noise, { octaves, frequency, persistence });
|
||||
const value = fbm.noise2D(x, y);
|
||||
|
||||
return { outputs: { value } };
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SeededRandom 节点 | SeededRandom Node
|
||||
// =============================================================================
|
||||
|
||||
export const SeededRandomTemplate: BlueprintNodeTemplate = {
|
||||
type: 'SeededRandom',
|
||||
title: 'Seeded Random',
|
||||
category: 'math',
|
||||
description: 'Generate deterministic random number / 生成确定性随机数',
|
||||
keywords: ['random', 'seed', 'deterministic', 'procedural'],
|
||||
menuPath: ['Procedural', 'Random', 'Seeded Random'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'seed', displayName: 'Seed', type: 'int' },
|
||||
{ name: 'index', displayName: 'Index', type: 'int' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'float' }
|
||||
],
|
||||
color: '#9c27b0'
|
||||
};
|
||||
|
||||
export class SeededRandomExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ProcGenContext;
|
||||
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
|
||||
const index = ctx.evaluateInput(node.id, 'index', 0) as number;
|
||||
|
||||
// Create deterministic value from seed and index
|
||||
const rng = new SeededRandom(seed + index * 12345);
|
||||
const value = rng.next();
|
||||
|
||||
return { outputs: { value } };
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SeededRandomInt 节点 | SeededRandomInt Node
|
||||
// =============================================================================
|
||||
|
||||
export const SeededRandomIntTemplate: BlueprintNodeTemplate = {
|
||||
type: 'SeededRandomInt',
|
||||
title: 'Seeded Random Int',
|
||||
category: 'math',
|
||||
description: 'Generate deterministic random integer / 生成确定性随机整数',
|
||||
keywords: ['random', 'seed', 'integer', 'deterministic'],
|
||||
menuPath: ['Procedural', 'Random', 'Seeded Random Int'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'seed', displayName: 'Seed', type: 'int' },
|
||||
{ name: 'index', displayName: 'Index', type: 'int' },
|
||||
{ name: 'min', displayName: 'Min', type: 'int' },
|
||||
{ name: 'max', displayName: 'Max', type: 'int' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'int' }
|
||||
],
|
||||
color: '#9c27b0'
|
||||
};
|
||||
|
||||
export class SeededRandomIntExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ProcGenContext;
|
||||
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
|
||||
const index = ctx.evaluateInput(node.id, 'index', 0) as number;
|
||||
const min = ctx.evaluateInput(node.id, 'min', 0) as number;
|
||||
const max = ctx.evaluateInput(node.id, 'max', 100) as number;
|
||||
|
||||
const rng = new SeededRandom(seed + index * 12345);
|
||||
const value = rng.nextInt(min, max);
|
||||
|
||||
return { outputs: { value } };
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WeightedPick 节点 | WeightedPick Node
|
||||
// =============================================================================
|
||||
|
||||
export const WeightedPickTemplate: BlueprintNodeTemplate = {
|
||||
type: 'WeightedPick',
|
||||
title: 'Weighted Pick',
|
||||
category: 'math',
|
||||
description: 'Pick from weighted options / 从加权选项中选择',
|
||||
keywords: ['random', 'weight', 'pick', 'select', 'loot'],
|
||||
menuPath: ['Procedural', 'Random', 'Weighted Pick'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'seed', displayName: 'Seed', type: 'int' },
|
||||
{ name: 'index', displayName: 'Index', type: 'int' },
|
||||
{ name: 'items', displayName: 'Items', type: 'array' },
|
||||
{ name: 'weights', displayName: 'Weights', type: 'array' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'any' },
|
||||
{ name: 'selectedIndex', displayName: 'Index', type: 'int' }
|
||||
],
|
||||
color: '#9c27b0'
|
||||
};
|
||||
|
||||
export class WeightedPickExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ProcGenContext;
|
||||
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
|
||||
const index = ctx.evaluateInput(node.id, 'index', 0) as number;
|
||||
const items = ctx.evaluateInput(node.id, 'items', []) as unknown[];
|
||||
const weights = ctx.evaluateInput(node.id, 'weights', []) as number[];
|
||||
|
||||
if (items.length === 0) {
|
||||
return { outputs: { value: null, selectedIndex: -1 } };
|
||||
}
|
||||
|
||||
const rng = new SeededRandom(seed + index * 12345);
|
||||
|
||||
// Build weighted items
|
||||
const weightedItems: WeightedItem<{ value: unknown; index: number }>[] = items.map((item, i) => ({
|
||||
value: { value: item, index: i },
|
||||
weight: weights[i] ?? 1
|
||||
}));
|
||||
|
||||
const result = weightedPick(weightedItems, rng);
|
||||
|
||||
return { outputs: { value: result.value, selectedIndex: result.index } };
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ShuffleArray 节点 | ShuffleArray Node
|
||||
// =============================================================================
|
||||
|
||||
export const ShuffleArrayTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ShuffleArray',
|
||||
title: 'Shuffle Array',
|
||||
category: 'math',
|
||||
description: 'Shuffle array with seed / 使用种子洗牌数组',
|
||||
keywords: ['random', 'shuffle', 'array', 'order'],
|
||||
menuPath: ['Procedural', 'Random', 'Shuffle Array'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'seed', displayName: 'Seed', type: 'int' },
|
||||
{ name: 'array', displayName: 'Array', type: 'array' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'array' }
|
||||
],
|
||||
color: '#9c27b0'
|
||||
};
|
||||
|
||||
export class ShuffleArrayExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ProcGenContext;
|
||||
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
|
||||
const array = ctx.evaluateInput(node.id, 'array', []) as unknown[];
|
||||
|
||||
const rng = new SeededRandom(seed);
|
||||
const result = shuffle([...array], rng);
|
||||
|
||||
return { outputs: { result } };
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PickRandom 节点 | PickRandom Node
|
||||
// =============================================================================
|
||||
|
||||
export const PickRandomTemplate: BlueprintNodeTemplate = {
|
||||
type: 'PickRandom',
|
||||
title: 'Pick Random',
|
||||
category: 'math',
|
||||
description: 'Pick random element from array / 从数组中随机选择元素',
|
||||
keywords: ['random', 'pick', 'array', 'select'],
|
||||
menuPath: ['Procedural', 'Random', 'Pick Random'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'seed', displayName: 'Seed', type: 'int' },
|
||||
{ name: 'index', displayName: 'Index', type: 'int' },
|
||||
{ name: 'array', displayName: 'Array', type: 'array' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'any' }
|
||||
],
|
||||
color: '#9c27b0'
|
||||
};
|
||||
|
||||
export class PickRandomExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ProcGenContext;
|
||||
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
|
||||
const index = ctx.evaluateInput(node.id, 'index', 0) as number;
|
||||
const array = ctx.evaluateInput(node.id, 'array', []) as unknown[];
|
||||
|
||||
if (array.length === 0) {
|
||||
return { outputs: { value: null } };
|
||||
}
|
||||
|
||||
const rng = new SeededRandom(seed + index * 12345);
|
||||
const value = pickOne(array, rng);
|
||||
|
||||
return { outputs: { value } };
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SampleArray 节点 | SampleArray Node
|
||||
// =============================================================================
|
||||
|
||||
export const SampleArrayTemplate: BlueprintNodeTemplate = {
|
||||
type: 'SampleArray',
|
||||
title: 'Sample Array',
|
||||
category: 'math',
|
||||
description: 'Sample N unique elements from array / 从数组中采样 N 个不重复元素',
|
||||
keywords: ['random', 'sample', 'array', 'unique'],
|
||||
menuPath: ['Procedural', 'Random', 'Sample Array'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'seed', displayName: 'Seed', type: 'int' },
|
||||
{ name: 'array', displayName: 'Array', type: 'array' },
|
||||
{ name: 'count', displayName: 'Count', type: 'int' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'array' }
|
||||
],
|
||||
color: '#9c27b0'
|
||||
};
|
||||
|
||||
export class SampleArrayExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ProcGenContext;
|
||||
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
|
||||
const array = ctx.evaluateInput(node.id, 'array', []) as unknown[];
|
||||
const count = ctx.evaluateInput(node.id, 'count', 1) as number;
|
||||
|
||||
if (array.length === 0 || count <= 0) {
|
||||
return { outputs: { result: [] } };
|
||||
}
|
||||
|
||||
const rng = new SeededRandom(seed);
|
||||
const actualCount = Math.min(count, array.length);
|
||||
const result = sample(array, actualCount, rng);
|
||||
|
||||
return { outputs: { result } };
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RandomPointInCircle 节点 | RandomPointInCircle Node
|
||||
// =============================================================================
|
||||
|
||||
export const RandomPointInCircleTemplate: BlueprintNodeTemplate = {
|
||||
type: 'RandomPointInCircle',
|
||||
title: 'Random Point In Circle',
|
||||
category: 'math',
|
||||
description: 'Generate random point inside circle / 在圆内生成随机点',
|
||||
keywords: ['random', 'point', 'circle', 'position'],
|
||||
menuPath: ['Procedural', 'Random', 'Random Point In Circle'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'seed', displayName: 'Seed', type: 'int' },
|
||||
{ name: 'index', displayName: 'Index', type: 'int' },
|
||||
{ name: 'centerX', displayName: 'Center X', type: 'float' },
|
||||
{ name: 'centerY', displayName: 'Center Y', type: 'float' },
|
||||
{ name: 'radius', displayName: 'Radius', type: 'float' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'x', displayName: 'X', type: 'float' },
|
||||
{ name: 'y', displayName: 'Y', type: 'float' }
|
||||
],
|
||||
color: '#9c27b0'
|
||||
};
|
||||
|
||||
export class RandomPointInCircleExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ProcGenContext;
|
||||
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
|
||||
const index = ctx.evaluateInput(node.id, 'index', 0) as number;
|
||||
const centerX = ctx.evaluateInput(node.id, 'centerX', 0) as number;
|
||||
const centerY = ctx.evaluateInput(node.id, 'centerY', 0) as number;
|
||||
const radius = ctx.evaluateInput(node.id, 'radius', 1) as number;
|
||||
|
||||
const rng = new SeededRandom(seed + index * 12345);
|
||||
const point = rng.nextPointInCircle(radius);
|
||||
|
||||
return {
|
||||
outputs: {
|
||||
x: centerX + point.x,
|
||||
y: centerY + point.y
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 节点定义集合 | Node Definition Collection
|
||||
// =============================================================================
|
||||
|
||||
export const ProcGenNodeDefinitions = {
|
||||
templates: [
|
||||
SampleNoise2DTemplate,
|
||||
SampleFBMTemplate,
|
||||
SeededRandomTemplate,
|
||||
SeededRandomIntTemplate,
|
||||
WeightedPickTemplate,
|
||||
ShuffleArrayTemplate,
|
||||
PickRandomTemplate,
|
||||
SampleArrayTemplate,
|
||||
RandomPointInCircleTemplate
|
||||
],
|
||||
executors: new Map<string, INodeExecutor>([
|
||||
['SampleNoise2D', new SampleNoise2DExecutor()],
|
||||
['SampleFBM', new SampleFBMExecutor()],
|
||||
['SeededRandom', new SeededRandomExecutor()],
|
||||
['SeededRandomInt', new SeededRandomIntExecutor()],
|
||||
['WeightedPick', new WeightedPickExecutor()],
|
||||
['ShuffleArray', new ShuffleArrayExecutor()],
|
||||
['PickRandom', new PickRandomExecutor()],
|
||||
['SampleArray', new SampleArrayExecutor()],
|
||||
['RandomPointInCircle', new RandomPointInCircleExecutor()]
|
||||
])
|
||||
};
|
||||
29
packages/framework/procgen/src/nodes/index.ts
Normal file
29
packages/framework/procgen/src/nodes/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @zh 程序化生成蓝图节点模块
|
||||
* @en Procedural Generation Blueprint Nodes Module
|
||||
*/
|
||||
|
||||
export {
|
||||
// Templates
|
||||
SampleNoise2DTemplate,
|
||||
SampleFBMTemplate,
|
||||
SeededRandomTemplate,
|
||||
SeededRandomIntTemplate,
|
||||
WeightedPickTemplate,
|
||||
ShuffleArrayTemplate,
|
||||
PickRandomTemplate,
|
||||
SampleArrayTemplate,
|
||||
RandomPointInCircleTemplate,
|
||||
// Executors
|
||||
SampleNoise2DExecutor,
|
||||
SampleFBMExecutor,
|
||||
SeededRandomExecutor,
|
||||
SeededRandomIntExecutor,
|
||||
WeightedPickExecutor,
|
||||
ShuffleArrayExecutor,
|
||||
PickRandomExecutor,
|
||||
SampleArrayExecutor,
|
||||
RandomPointInCircleExecutor,
|
||||
// Collection
|
||||
ProcGenNodeDefinitions
|
||||
} from './ProcGenNodes';
|
||||
214
packages/framework/procgen/src/noise/FBM.ts
Normal file
214
packages/framework/procgen/src/noise/FBM.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* @zh 分形布朗运动 (FBM)
|
||||
* @en Fractal Brownian Motion (FBM)
|
||||
*
|
||||
* @zh 通过叠加多层噪声创建更自然的效果
|
||||
* @en Creates more natural effects by layering multiple noise octaves
|
||||
*/
|
||||
|
||||
/**
|
||||
* @zh 噪声函数接口
|
||||
* @en Noise function interface
|
||||
*/
|
||||
export interface INoise2D {
|
||||
noise2D(x: number, y: number): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 噪声函数接口 (3D)
|
||||
* @en Noise function interface (3D)
|
||||
*/
|
||||
export interface INoise3D {
|
||||
noise3D(x: number, y: number, z: number): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh FBM 配置
|
||||
* @en FBM configuration
|
||||
*/
|
||||
export interface FBMConfig {
|
||||
/**
|
||||
* @zh 八度数(层数)
|
||||
* @en Number of octaves (layers)
|
||||
*/
|
||||
octaves: number;
|
||||
|
||||
/**
|
||||
* @zh 频率倍增因子
|
||||
* @en Frequency multiplier per octave
|
||||
*/
|
||||
lacunarity: number;
|
||||
|
||||
/**
|
||||
* @zh 振幅衰减因子
|
||||
* @en Amplitude decay per octave
|
||||
*/
|
||||
persistence: number;
|
||||
|
||||
/**
|
||||
* @zh 初始频率
|
||||
* @en Initial frequency
|
||||
*/
|
||||
frequency: number;
|
||||
|
||||
/**
|
||||
* @zh 初始振幅
|
||||
* @en Initial amplitude
|
||||
*/
|
||||
amplitude: number;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: FBMConfig = {
|
||||
octaves: 6,
|
||||
lacunarity: 2.0,
|
||||
persistence: 0.5,
|
||||
frequency: 1.0,
|
||||
amplitude: 1.0
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh FBM 噪声生成器
|
||||
* @en FBM noise generator
|
||||
*/
|
||||
export class FBM {
|
||||
private readonly _noise: INoise2D & Partial<INoise3D>;
|
||||
private readonly _config: FBMConfig;
|
||||
|
||||
/**
|
||||
* @zh 创建 FBM 噪声生成器
|
||||
* @en Create FBM noise generator
|
||||
*
|
||||
* @param noise - @zh 基础噪声函数 @en Base noise function
|
||||
* @param config - @zh 配置 @en Configuration
|
||||
*/
|
||||
constructor(noise: INoise2D & Partial<INoise3D>, config?: Partial<FBMConfig>) {
|
||||
this._noise = noise;
|
||||
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 2D FBM 噪声
|
||||
* @en 2D FBM noise
|
||||
*
|
||||
* @param x - @zh X 坐标 @en X coordinate
|
||||
* @param y - @zh Y 坐标 @en Y coordinate
|
||||
* @returns @zh 噪声值 @en Noise value
|
||||
*/
|
||||
noise2D(x: number, y: number): number {
|
||||
let value = 0;
|
||||
let frequency = this._config.frequency;
|
||||
let amplitude = this._config.amplitude;
|
||||
let maxValue = 0;
|
||||
|
||||
for (let i = 0; i < this._config.octaves; i++) {
|
||||
value += this._noise.noise2D(x * frequency, y * frequency) * amplitude;
|
||||
maxValue += amplitude;
|
||||
amplitude *= this._config.persistence;
|
||||
frequency *= this._config.lacunarity;
|
||||
}
|
||||
|
||||
return value / maxValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 3D FBM 噪声
|
||||
* @en 3D FBM noise
|
||||
*
|
||||
* @param x - @zh X 坐标 @en X coordinate
|
||||
* @param y - @zh Y 坐标 @en Y coordinate
|
||||
* @param z - @zh Z 坐标 @en Z coordinate
|
||||
* @returns @zh 噪声值 @en Noise value
|
||||
*/
|
||||
noise3D(x: number, y: number, z: number): number {
|
||||
if (!this._noise.noise3D) {
|
||||
throw new Error('Base noise does not support 3D');
|
||||
}
|
||||
|
||||
let value = 0;
|
||||
let frequency = this._config.frequency;
|
||||
let amplitude = this._config.amplitude;
|
||||
let maxValue = 0;
|
||||
|
||||
for (let i = 0; i < this._config.octaves; i++) {
|
||||
value += this._noise.noise3D(x * frequency, y * frequency, z * frequency) * amplitude;
|
||||
maxValue += amplitude;
|
||||
amplitude *= this._config.persistence;
|
||||
frequency *= this._config.lacunarity;
|
||||
}
|
||||
|
||||
return value / maxValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh Ridged FBM(脊状,适合山脉)
|
||||
* @en Ridged FBM (suitable for mountains)
|
||||
*/
|
||||
ridged2D(x: number, y: number): number {
|
||||
let value = 0;
|
||||
let frequency = this._config.frequency;
|
||||
let amplitude = this._config.amplitude;
|
||||
let weight = 1;
|
||||
|
||||
for (let i = 0; i < this._config.octaves; i++) {
|
||||
let signal = this._noise.noise2D(x * frequency, y * frequency);
|
||||
signal = 1 - Math.abs(signal);
|
||||
signal *= signal;
|
||||
signal *= weight;
|
||||
weight = Math.max(0, Math.min(1, signal * 2));
|
||||
value += signal * amplitude;
|
||||
frequency *= this._config.lacunarity;
|
||||
amplitude *= this._config.persistence;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh Turbulence(湍流,使用绝对值)
|
||||
* @en Turbulence (using absolute value)
|
||||
*/
|
||||
turbulence2D(x: number, y: number): number {
|
||||
let value = 0;
|
||||
let frequency = this._config.frequency;
|
||||
let amplitude = this._config.amplitude;
|
||||
let maxValue = 0;
|
||||
|
||||
for (let i = 0; i < this._config.octaves; i++) {
|
||||
value += Math.abs(this._noise.noise2D(x * frequency, y * frequency)) * amplitude;
|
||||
maxValue += amplitude;
|
||||
amplitude *= this._config.persistence;
|
||||
frequency *= this._config.lacunarity;
|
||||
}
|
||||
|
||||
return value / maxValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh Billowed(膨胀,适合云朵)
|
||||
* @en Billowed (suitable for clouds)
|
||||
*/
|
||||
billowed2D(x: number, y: number): number {
|
||||
let value = 0;
|
||||
let frequency = this._config.frequency;
|
||||
let amplitude = this._config.amplitude;
|
||||
let maxValue = 0;
|
||||
|
||||
for (let i = 0; i < this._config.octaves; i++) {
|
||||
const n = this._noise.noise2D(x * frequency, y * frequency);
|
||||
value += (Math.abs(n) * 2 - 1) * amplitude;
|
||||
maxValue += amplitude;
|
||||
amplitude *= this._config.persistence;
|
||||
frequency *= this._config.lacunarity;
|
||||
}
|
||||
|
||||
return value / maxValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建 FBM 噪声生成器
|
||||
* @en Create FBM noise generator
|
||||
*/
|
||||
export function createFBM(noise: INoise2D & Partial<INoise3D>, config?: Partial<FBMConfig>): FBM {
|
||||
return new FBM(noise, config);
|
||||
}
|
||||
160
packages/framework/procgen/src/noise/PerlinNoise.ts
Normal file
160
packages/framework/procgen/src/noise/PerlinNoise.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* @zh Perlin 噪声实现
|
||||
* @en Perlin Noise Implementation
|
||||
*
|
||||
* @zh 基于 Ken Perlin 的改进版噪声算法
|
||||
* @en Based on Ken Perlin's improved noise algorithm
|
||||
*/
|
||||
|
||||
/**
|
||||
* @zh Perlin 噪声生成器
|
||||
* @en Perlin noise generator
|
||||
*/
|
||||
export class PerlinNoise {
|
||||
private readonly _perm: Uint8Array;
|
||||
private readonly _gradP: Float32Array;
|
||||
|
||||
/**
|
||||
* @zh 创建 Perlin 噪声生成器
|
||||
* @en Create Perlin noise generator
|
||||
*
|
||||
* @param seed - @zh 随机种子 @en Random seed
|
||||
*/
|
||||
constructor(seed: number = 0) {
|
||||
this._perm = new Uint8Array(512);
|
||||
this._gradP = new Float32Array(512 * 3);
|
||||
this._seed(seed);
|
||||
}
|
||||
|
||||
private _seed(seed: number): void {
|
||||
const p = new Uint8Array(256);
|
||||
|
||||
// Initialize permutation array
|
||||
for (let i = 0; i < 256; i++) {
|
||||
p[i] = i;
|
||||
}
|
||||
|
||||
// Shuffle using seed
|
||||
let n = seed;
|
||||
for (let i = 255; i > 0; i--) {
|
||||
n = (n * 16807) % 2147483647;
|
||||
const j = n % (i + 1);
|
||||
[p[i], p[j]] = [p[j], p[i]];
|
||||
}
|
||||
|
||||
// Duplicate permutation array
|
||||
for (let i = 0; i < 512; i++) {
|
||||
this._perm[i] = p[i & 255];
|
||||
}
|
||||
|
||||
// Precompute gradient vectors
|
||||
const grad3 = [
|
||||
1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1, 0,
|
||||
1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, -1,
|
||||
0, 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1
|
||||
];
|
||||
|
||||
for (let i = 0; i < 512; i++) {
|
||||
const gi = (this._perm[i] % 12) * 3;
|
||||
this._gradP[i * 3] = grad3[gi];
|
||||
this._gradP[i * 3 + 1] = grad3[gi + 1];
|
||||
this._gradP[i * 3 + 2] = grad3[gi + 2];
|
||||
}
|
||||
}
|
||||
|
||||
private _fade(t: number): number {
|
||||
return t * t * t * (t * (t * 6 - 15) + 10);
|
||||
}
|
||||
|
||||
private _lerp(a: number, b: number, t: number): number {
|
||||
return a + t * (b - a);
|
||||
}
|
||||
|
||||
private _dot2(gi: number, x: number, y: number): number {
|
||||
return this._gradP[gi * 3] * x + this._gradP[gi * 3 + 1] * y;
|
||||
}
|
||||
|
||||
private _dot3(gi: number, x: number, y: number, z: number): number {
|
||||
return this._gradP[gi * 3] * x + this._gradP[gi * 3 + 1] * y + this._gradP[gi * 3 + 2] * z;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 2D Perlin 噪声
|
||||
* @en 2D Perlin noise
|
||||
*
|
||||
* @param x - @zh X 坐标 @en X coordinate
|
||||
* @param y - @zh Y 坐标 @en Y coordinate
|
||||
* @returns @zh 噪声值 [-1, 1] @en Noise value [-1, 1]
|
||||
*/
|
||||
noise2D(x: number, y: number): number {
|
||||
const X = Math.floor(x) & 255;
|
||||
const Y = Math.floor(y) & 255;
|
||||
|
||||
x -= Math.floor(x);
|
||||
y -= Math.floor(y);
|
||||
|
||||
const u = this._fade(x);
|
||||
const v = this._fade(y);
|
||||
|
||||
const A = this._perm[X] + Y;
|
||||
const B = this._perm[X + 1] + Y;
|
||||
|
||||
return this._lerp(
|
||||
this._lerp(this._dot2(this._perm[A], x, y), this._dot2(this._perm[B], x - 1, y), u),
|
||||
this._lerp(this._dot2(this._perm[A + 1], x, y - 1), this._dot2(this._perm[B + 1], x - 1, y - 1), u),
|
||||
v
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 3D Perlin 噪声
|
||||
* @en 3D Perlin noise
|
||||
*
|
||||
* @param x - @zh X 坐标 @en X coordinate
|
||||
* @param y - @zh Y 坐标 @en Y coordinate
|
||||
* @param z - @zh Z 坐标 @en Z coordinate
|
||||
* @returns @zh 噪声值 [-1, 1] @en Noise value [-1, 1]
|
||||
*/
|
||||
noise3D(x: number, y: number, z: number): number {
|
||||
const X = Math.floor(x) & 255;
|
||||
const Y = Math.floor(y) & 255;
|
||||
const Z = Math.floor(z) & 255;
|
||||
|
||||
x -= Math.floor(x);
|
||||
y -= Math.floor(y);
|
||||
z -= Math.floor(z);
|
||||
|
||||
const u = this._fade(x);
|
||||
const v = this._fade(y);
|
||||
const w = this._fade(z);
|
||||
|
||||
const A = this._perm[X] + Y;
|
||||
const AA = this._perm[A] + Z;
|
||||
const AB = this._perm[A + 1] + Z;
|
||||
const B = this._perm[X + 1] + Y;
|
||||
const BA = this._perm[B] + Z;
|
||||
const BB = this._perm[B + 1] + Z;
|
||||
|
||||
return this._lerp(
|
||||
this._lerp(
|
||||
this._lerp(this._dot3(this._perm[AA], x, y, z), this._dot3(this._perm[BA], x - 1, y, z), u),
|
||||
this._lerp(this._dot3(this._perm[AB], x, y - 1, z), this._dot3(this._perm[BB], x - 1, y - 1, z), u),
|
||||
v
|
||||
),
|
||||
this._lerp(
|
||||
this._lerp(this._dot3(this._perm[AA + 1], x, y, z - 1), this._dot3(this._perm[BA + 1], x - 1, y, z - 1), u),
|
||||
this._lerp(this._dot3(this._perm[AB + 1], x, y - 1, z - 1), this._dot3(this._perm[BB + 1], x - 1, y - 1, z - 1), u),
|
||||
v
|
||||
),
|
||||
w
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建 Perlin 噪声生成器
|
||||
* @en Create Perlin noise generator
|
||||
*/
|
||||
export function createPerlinNoise(seed: number = 0): PerlinNoise {
|
||||
return new PerlinNoise(seed);
|
||||
}
|
||||
222
packages/framework/procgen/src/noise/SimplexNoise.ts
Normal file
222
packages/framework/procgen/src/noise/SimplexNoise.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* @zh Simplex 噪声实现
|
||||
* @en Simplex Noise Implementation
|
||||
*
|
||||
* @zh 比 Perlin 噪声更快且没有方向性伪影
|
||||
* @en Faster than Perlin noise with no directional artifacts
|
||||
*/
|
||||
|
||||
const F2 = 0.5 * (Math.sqrt(3) - 1);
|
||||
const G2 = (3 - Math.sqrt(3)) / 6;
|
||||
const F3 = 1 / 3;
|
||||
const G3 = 1 / 6;
|
||||
|
||||
/**
|
||||
* @zh Simplex 噪声生成器
|
||||
* @en Simplex noise generator
|
||||
*/
|
||||
export class SimplexNoise {
|
||||
private readonly _perm: Uint8Array;
|
||||
private readonly _permMod12: Uint8Array;
|
||||
|
||||
private static readonly _grad3 = new Float32Array([
|
||||
1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1, 0,
|
||||
1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, -1,
|
||||
0, 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1
|
||||
]);
|
||||
|
||||
/**
|
||||
* @zh 创建 Simplex 噪声生成器
|
||||
* @en Create Simplex noise generator
|
||||
*
|
||||
* @param seed - @zh 随机种子 @en Random seed
|
||||
*/
|
||||
constructor(seed: number = 0) {
|
||||
this._perm = new Uint8Array(512);
|
||||
this._permMod12 = new Uint8Array(512);
|
||||
this._seed(seed);
|
||||
}
|
||||
|
||||
private _seed(seed: number): void {
|
||||
const p = new Uint8Array(256);
|
||||
|
||||
for (let i = 0; i < 256; i++) {
|
||||
p[i] = i;
|
||||
}
|
||||
|
||||
let n = seed;
|
||||
for (let i = 255; i > 0; i--) {
|
||||
n = (n * 16807) % 2147483647;
|
||||
const j = n % (i + 1);
|
||||
[p[i], p[j]] = [p[j], p[i]];
|
||||
}
|
||||
|
||||
for (let i = 0; i < 512; i++) {
|
||||
this._perm[i] = p[i & 255];
|
||||
this._permMod12[i] = this._perm[i] % 12;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 2D Simplex 噪声
|
||||
* @en 2D Simplex noise
|
||||
*
|
||||
* @param x - @zh X 坐标 @en X coordinate
|
||||
* @param y - @zh Y 坐标 @en Y coordinate
|
||||
* @returns @zh 噪声值 [-1, 1] @en Noise value [-1, 1]
|
||||
*/
|
||||
noise2D(x: number, y: number): number {
|
||||
const grad3 = SimplexNoise._grad3;
|
||||
let n0 = 0, n1 = 0, n2 = 0;
|
||||
|
||||
const s = (x + y) * F2;
|
||||
const i = Math.floor(x + s);
|
||||
const j = Math.floor(y + s);
|
||||
|
||||
const t = (i + j) * G2;
|
||||
const X0 = i - t;
|
||||
const Y0 = j - t;
|
||||
const x0 = x - X0;
|
||||
const y0 = y - Y0;
|
||||
|
||||
let i1: number, j1: number;
|
||||
if (x0 > y0) {
|
||||
i1 = 1;
|
||||
j1 = 0;
|
||||
} else {
|
||||
i1 = 0;
|
||||
j1 = 1;
|
||||
}
|
||||
|
||||
const x1 = x0 - i1 + G2;
|
||||
const y1 = y0 - j1 + G2;
|
||||
const x2 = x0 - 1 + 2 * G2;
|
||||
const y2 = y0 - 1 + 2 * G2;
|
||||
|
||||
const ii = i & 255;
|
||||
const jj = j & 255;
|
||||
const gi0 = this._permMod12[ii + this._perm[jj]] * 3;
|
||||
const gi1 = this._permMod12[ii + i1 + this._perm[jj + j1]] * 3;
|
||||
const gi2 = this._permMod12[ii + 1 + this._perm[jj + 1]] * 3;
|
||||
|
||||
let t0 = 0.5 - x0 * x0 - y0 * y0;
|
||||
if (t0 >= 0) {
|
||||
t0 *= t0;
|
||||
n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0);
|
||||
}
|
||||
|
||||
let t1 = 0.5 - x1 * x1 - y1 * y1;
|
||||
if (t1 >= 0) {
|
||||
t1 *= t1;
|
||||
n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1);
|
||||
}
|
||||
|
||||
let t2 = 0.5 - x2 * x2 - y2 * y2;
|
||||
if (t2 >= 0) {
|
||||
t2 *= t2;
|
||||
n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2);
|
||||
}
|
||||
|
||||
return 70 * (n0 + n1 + n2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 3D Simplex 噪声
|
||||
* @en 3D Simplex noise
|
||||
*
|
||||
* @param x - @zh X 坐标 @en X coordinate
|
||||
* @param y - @zh Y 坐标 @en Y coordinate
|
||||
* @param z - @zh Z 坐标 @en Z coordinate
|
||||
* @returns @zh 噪声值 [-1, 1] @en Noise value [-1, 1]
|
||||
*/
|
||||
noise3D(x: number, y: number, z: number): number {
|
||||
const grad3 = SimplexNoise._grad3;
|
||||
let n0 = 0, n1 = 0, n2 = 0, n3 = 0;
|
||||
|
||||
const s = (x + y + z) * F3;
|
||||
const i = Math.floor(x + s);
|
||||
const j = Math.floor(y + s);
|
||||
const k = Math.floor(z + s);
|
||||
|
||||
const t = (i + j + k) * G3;
|
||||
const X0 = i - t;
|
||||
const Y0 = j - t;
|
||||
const Z0 = k - t;
|
||||
const x0 = x - X0;
|
||||
const y0 = y - Y0;
|
||||
const z0 = z - Z0;
|
||||
|
||||
let i1: number, j1: number, k1: number;
|
||||
let i2: number, j2: number, k2: number;
|
||||
|
||||
if (x0 >= y0) {
|
||||
if (y0 >= z0) {
|
||||
i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 1; k2 = 0;
|
||||
} else if (x0 >= z0) {
|
||||
i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 0; k2 = 1;
|
||||
} else {
|
||||
i1 = 0; j1 = 0; k1 = 1; i2 = 1; j2 = 0; k2 = 1;
|
||||
}
|
||||
} else {
|
||||
if (y0 < z0) {
|
||||
i1 = 0; j1 = 0; k1 = 1; i2 = 0; j2 = 1; k2 = 1;
|
||||
} else if (x0 < z0) {
|
||||
i1 = 0; j1 = 1; k1 = 0; i2 = 0; j2 = 1; k2 = 1;
|
||||
} else {
|
||||
i1 = 0; j1 = 1; k1 = 0; i2 = 1; j2 = 1; k2 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const x1 = x0 - i1 + G3;
|
||||
const y1 = y0 - j1 + G3;
|
||||
const z1 = z0 - k1 + G3;
|
||||
const x2 = x0 - i2 + 2 * G3;
|
||||
const y2 = y0 - j2 + 2 * G3;
|
||||
const z2 = z0 - k2 + 2 * G3;
|
||||
const x3 = x0 - 1 + 3 * G3;
|
||||
const y3 = y0 - 1 + 3 * G3;
|
||||
const z3 = z0 - 1 + 3 * G3;
|
||||
|
||||
const ii = i & 255;
|
||||
const jj = j & 255;
|
||||
const kk = k & 255;
|
||||
const gi0 = this._permMod12[ii + this._perm[jj + this._perm[kk]]] * 3;
|
||||
const gi1 = this._permMod12[ii + i1 + this._perm[jj + j1 + this._perm[kk + k1]]] * 3;
|
||||
const gi2 = this._permMod12[ii + i2 + this._perm[jj + j2 + this._perm[kk + k2]]] * 3;
|
||||
const gi3 = this._permMod12[ii + 1 + this._perm[jj + 1 + this._perm[kk + 1]]] * 3;
|
||||
|
||||
let t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0;
|
||||
if (t0 >= 0) {
|
||||
t0 *= t0;
|
||||
n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0 + grad3[gi0 + 2] * z0);
|
||||
}
|
||||
|
||||
let t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;
|
||||
if (t1 >= 0) {
|
||||
t1 *= t1;
|
||||
n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1 + grad3[gi1 + 2] * z1);
|
||||
}
|
||||
|
||||
let t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;
|
||||
if (t2 >= 0) {
|
||||
t2 *= t2;
|
||||
n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2 + grad3[gi2 + 2] * z2);
|
||||
}
|
||||
|
||||
let t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;
|
||||
if (t3 >= 0) {
|
||||
t3 *= t3;
|
||||
n3 = t3 * t3 * (grad3[gi3] * x3 + grad3[gi3 + 1] * y3 + grad3[gi3 + 2] * z3);
|
||||
}
|
||||
|
||||
return 32 * (n0 + n1 + n2 + n3);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建 Simplex 噪声生成器
|
||||
* @en Create Simplex noise generator
|
||||
*/
|
||||
export function createSimplexNoise(seed: number = 0): SimplexNoise {
|
||||
return new SimplexNoise(seed);
|
||||
}
|
||||
196
packages/framework/procgen/src/noise/WorleyNoise.ts
Normal file
196
packages/framework/procgen/src/noise/WorleyNoise.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* @zh Worley (Cellular) 噪声实现
|
||||
* @en Worley (Cellular) Noise Implementation
|
||||
*
|
||||
* @zh 基于 Voronoi 图的噪声,适合生成细胞、石头纹理
|
||||
* @en Voronoi-based noise, suitable for cellular and stone textures
|
||||
*/
|
||||
|
||||
/**
|
||||
* @zh 距离函数类型
|
||||
* @en Distance function type
|
||||
*/
|
||||
export type DistanceFunction = 'euclidean' | 'manhattan' | 'chebyshev';
|
||||
|
||||
/**
|
||||
* @zh Worley 噪声生成器
|
||||
* @en Worley noise generator
|
||||
*/
|
||||
export class WorleyNoise {
|
||||
private readonly _seed: number;
|
||||
private readonly _distanceFunc: DistanceFunction;
|
||||
|
||||
/**
|
||||
* @zh 创建 Worley 噪声生成器
|
||||
* @en Create Worley noise generator
|
||||
*
|
||||
* @param seed - @zh 随机种子 @en Random seed
|
||||
* @param distanceFunc - @zh 距离函数 @en Distance function
|
||||
*/
|
||||
constructor(seed: number = 0, distanceFunc: DistanceFunction = 'euclidean') {
|
||||
this._seed = seed;
|
||||
this._distanceFunc = distanceFunc;
|
||||
}
|
||||
|
||||
private _hash(x: number, y: number, z: number = 0): number {
|
||||
let h = this._seed;
|
||||
h ^= x * 374761393;
|
||||
h ^= y * 668265263;
|
||||
h ^= z * 1274126177;
|
||||
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
||||
return h;
|
||||
}
|
||||
|
||||
private _randomPoint(cellX: number, cellY: number, index: number): { x: number; y: number } {
|
||||
const h1 = this._hash(cellX, cellY, index);
|
||||
const h2 = this._hash(cellX, cellY, index + 1000);
|
||||
return {
|
||||
x: cellX + (h1 & 0xFFFF) / 65536,
|
||||
y: cellY + (h2 & 0xFFFF) / 65536
|
||||
};
|
||||
}
|
||||
|
||||
private _randomPoint3D(cellX: number, cellY: number, cellZ: number, index: number): { x: number; y: number; z: number } {
|
||||
const h1 = this._hash(cellX, cellY, cellZ * 1000 + index);
|
||||
const h2 = this._hash(cellX, cellY, cellZ * 1000 + index + 1000);
|
||||
const h3 = this._hash(cellX, cellY, cellZ * 1000 + index + 2000);
|
||||
return {
|
||||
x: cellX + (h1 & 0xFFFF) / 65536,
|
||||
y: cellY + (h2 & 0xFFFF) / 65536,
|
||||
z: cellZ + (h3 & 0xFFFF) / 65536
|
||||
};
|
||||
}
|
||||
|
||||
private _distance2D(x1: number, y1: number, x2: number, y2: number): number {
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
|
||||
switch (this._distanceFunc) {
|
||||
case 'manhattan':
|
||||
return Math.abs(dx) + Math.abs(dy);
|
||||
case 'chebyshev':
|
||||
return Math.max(Math.abs(dx), Math.abs(dy));
|
||||
case 'euclidean':
|
||||
default:
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
}
|
||||
|
||||
private _distance3D(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number): number {
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const dz = z2 - z1;
|
||||
|
||||
switch (this._distanceFunc) {
|
||||
case 'manhattan':
|
||||
return Math.abs(dx) + Math.abs(dy) + Math.abs(dz);
|
||||
case 'chebyshev':
|
||||
return Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz));
|
||||
case 'euclidean':
|
||||
default:
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 2D Worley 噪声
|
||||
* @en 2D Worley noise
|
||||
*
|
||||
* @param x - @zh X 坐标 @en X coordinate
|
||||
* @param y - @zh Y 坐标 @en Y coordinate
|
||||
* @param pointsPerCell - @zh 每个单元格的点数 @en Points per cell
|
||||
* @returns @zh 到最近点的距离 [0, ~1.4] @en Distance to nearest point [0, ~1.4]
|
||||
*/
|
||||
noise2D(x: number, y: number, pointsPerCell: number = 1): number {
|
||||
const cellX = Math.floor(x);
|
||||
const cellY = Math.floor(y);
|
||||
|
||||
let minDist = Infinity;
|
||||
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
const cx = cellX + dx;
|
||||
const cy = cellY + dy;
|
||||
|
||||
for (let i = 0; i < pointsPerCell; i++) {
|
||||
const point = this._randomPoint(cx, cy, i);
|
||||
const dist = this._distance2D(x, y, point.x, point.y);
|
||||
minDist = Math.min(minDist, dist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return minDist;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 3D Worley 噪声
|
||||
* @en 3D Worley noise
|
||||
*
|
||||
* @param x - @zh X 坐标 @en X coordinate
|
||||
* @param y - @zh Y 坐标 @en Y coordinate
|
||||
* @param z - @zh Z 坐标 @en Z coordinate
|
||||
* @param pointsPerCell - @zh 每个单元格的点数 @en Points per cell
|
||||
* @returns @zh 到最近点的距离 @en Distance to nearest point
|
||||
*/
|
||||
noise3D(x: number, y: number, z: number, pointsPerCell: number = 1): number {
|
||||
const cellX = Math.floor(x);
|
||||
const cellY = Math.floor(y);
|
||||
const cellZ = Math.floor(z);
|
||||
|
||||
let minDist = Infinity;
|
||||
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
for (let dz = -1; dz <= 1; dz++) {
|
||||
const cx = cellX + dx;
|
||||
const cy = cellY + dy;
|
||||
const cz = cellZ + dz;
|
||||
|
||||
for (let i = 0; i < pointsPerCell; i++) {
|
||||
const point = this._randomPoint3D(cx, cy, cz, i);
|
||||
const dist = this._distance3D(x, y, z, point.x, point.y, point.z);
|
||||
minDist = Math.min(minDist, dist);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return minDist;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取到第 N 近点的距离(用于更复杂的纹理)
|
||||
* @en Get distance to Nth nearest point (for more complex textures)
|
||||
*
|
||||
* @param x - @zh X 坐标 @en X coordinate
|
||||
* @param y - @zh Y 坐标 @en Y coordinate
|
||||
* @param n - @zh 第 N 近 (1 = 最近) @en Nth nearest (1 = nearest)
|
||||
* @returns @zh 距离值 @en Distance value
|
||||
*/
|
||||
nthNearest2D(x: number, y: number, n: number = 1): number {
|
||||
const cellX = Math.floor(x);
|
||||
const cellY = Math.floor(y);
|
||||
const distances: number[] = [];
|
||||
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
const cx = cellX + dx;
|
||||
const cy = cellY + dy;
|
||||
const point = this._randomPoint(cx, cy, 0);
|
||||
distances.push(this._distance2D(x, y, point.x, point.y));
|
||||
}
|
||||
}
|
||||
|
||||
distances.sort((a, b) => a - b);
|
||||
return distances[Math.min(n - 1, distances.length - 1)];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建 Worley 噪声生成器
|
||||
* @en Create Worley noise generator
|
||||
*/
|
||||
export function createWorleyNoise(seed: number = 0, distanceFunc: DistanceFunction = 'euclidean'): WorleyNoise {
|
||||
return new WorleyNoise(seed, distanceFunc);
|
||||
}
|
||||
11
packages/framework/procgen/src/noise/index.ts
Normal file
11
packages/framework/procgen/src/noise/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @zh 噪声函数模块
|
||||
* @en Noise Functions Module
|
||||
*/
|
||||
|
||||
export { PerlinNoise, createPerlinNoise } from './PerlinNoise';
|
||||
export { SimplexNoise, createSimplexNoise } from './SimplexNoise';
|
||||
export { WorleyNoise, createWorleyNoise } from './WorleyNoise';
|
||||
export type { DistanceFunction } from './WorleyNoise';
|
||||
export { FBM, createFBM } from './FBM';
|
||||
export type { INoise2D, INoise3D, FBMConfig } from './FBM';
|
||||
192
packages/framework/procgen/src/random/SeededRandom.ts
Normal file
192
packages/framework/procgen/src/random/SeededRandom.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* @zh 种子随机数生成器
|
||||
* @en Seeded Random Number Generator
|
||||
*
|
||||
* @zh 基于 xorshift128+ 算法的确定性伪随机数生成器
|
||||
* @en Deterministic PRNG based on xorshift128+ algorithm
|
||||
*/
|
||||
|
||||
/**
|
||||
* @zh 种子随机数生成器
|
||||
* @en Seeded random number generator
|
||||
*/
|
||||
export class SeededRandom {
|
||||
private _s0: number;
|
||||
private _s1: number;
|
||||
private readonly _initialS0: number;
|
||||
private readonly _initialS1: number;
|
||||
|
||||
/**
|
||||
* @zh 创建种子随机数生成器
|
||||
* @en Create seeded random number generator
|
||||
*
|
||||
* @param seed - @zh 随机种子 @en Random seed
|
||||
*/
|
||||
constructor(seed: number = Date.now()) {
|
||||
// Initialize with MurmurHash3 mixing
|
||||
let h = seed | 0;
|
||||
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
||||
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
||||
h ^= h >>> 16;
|
||||
|
||||
this._s0 = h >>> 0;
|
||||
this._s1 = (h * 0x9e3779b9) >>> 0;
|
||||
|
||||
// Ensure non-zero state
|
||||
if (this._s0 === 0) this._s0 = 1;
|
||||
if (this._s1 === 0) this._s1 = 1;
|
||||
|
||||
this._initialS0 = this._s0;
|
||||
this._initialS1 = this._s1;
|
||||
|
||||
// Warm up
|
||||
for (let i = 0; i < 10; i++) {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置到初始状态
|
||||
* @en Reset to initial state
|
||||
*/
|
||||
reset(): void {
|
||||
this._s0 = this._initialS0;
|
||||
this._s1 = this._initialS1;
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 生成下一个随机数 [0, 1)
|
||||
* @en Generate next random number [0, 1)
|
||||
*/
|
||||
next(): number {
|
||||
let s1 = this._s0;
|
||||
const s0 = this._s1;
|
||||
this._s0 = s0;
|
||||
s1 ^= s1 << 23;
|
||||
s1 ^= s1 >>> 17;
|
||||
s1 ^= s0;
|
||||
s1 ^= s0 >>> 26;
|
||||
this._s1 = s1;
|
||||
return ((this._s0 + this._s1) >>> 0) / 4294967296;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 生成整数 [min, max]
|
||||
* @en Generate integer [min, max]
|
||||
*/
|
||||
nextInt(min: number, max: number): number {
|
||||
return Math.floor(this.next() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 生成浮点数 [min, max)
|
||||
* @en Generate float [min, max)
|
||||
*/
|
||||
nextFloat(min: number, max: number): number {
|
||||
return this.next() * (max - min) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 生成布尔值
|
||||
* @en Generate boolean
|
||||
*
|
||||
* @param probability - @zh 为 true 的概率 [0, 1] @en Probability of true [0, 1]
|
||||
*/
|
||||
nextBool(probability: number = 0.5): boolean {
|
||||
return this.next() < probability;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 生成正态分布随机数 (Box-Muller 变换)
|
||||
* @en Generate normally distributed random number (Box-Muller transform)
|
||||
*
|
||||
* @param mean - @zh 均值 @en Mean
|
||||
* @param stdDev - @zh 标准差 @en Standard deviation
|
||||
*/
|
||||
nextGaussian(mean: number = 0, stdDev: number = 1): number {
|
||||
const u1 = this.next();
|
||||
const u2 = this.next();
|
||||
const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
||||
return z0 * stdDev + mean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 生成指数分布随机数
|
||||
* @en Generate exponentially distributed random number
|
||||
*
|
||||
* @param lambda - @zh 率参数 @en Rate parameter
|
||||
*/
|
||||
nextExponential(lambda: number = 1): number {
|
||||
return -Math.log(1 - this.next()) / lambda;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 在圆内生成均匀分布的随机点
|
||||
* @en Generate uniformly distributed random point in circle
|
||||
*
|
||||
* @param radius - @zh 半径 @en Radius
|
||||
*/
|
||||
nextPointInCircle(radius: number = 1): { x: number; y: number } {
|
||||
const r = Math.sqrt(this.next()) * radius;
|
||||
const theta = this.next() * 2 * Math.PI;
|
||||
return {
|
||||
x: r * Math.cos(theta),
|
||||
y: r * Math.sin(theta)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 在圆环上生成随机点
|
||||
* @en Generate random point on circle
|
||||
*
|
||||
* @param radius - @zh 半径 @en Radius
|
||||
*/
|
||||
nextPointOnCircle(radius: number = 1): { x: number; y: number } {
|
||||
const theta = this.next() * 2 * Math.PI;
|
||||
return {
|
||||
x: radius * Math.cos(theta),
|
||||
y: radius * Math.sin(theta)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 在球内生成均匀分布的随机点
|
||||
* @en Generate uniformly distributed random point in sphere
|
||||
*
|
||||
* @param radius - @zh 半径 @en Radius
|
||||
*/
|
||||
nextPointInSphere(radius: number = 1): { x: number; y: number; z: number } {
|
||||
const r = Math.cbrt(this.next()) * radius;
|
||||
const theta = this.next() * 2 * Math.PI;
|
||||
const phi = Math.acos(2 * this.next() - 1);
|
||||
return {
|
||||
x: r * Math.sin(phi) * Math.cos(theta),
|
||||
y: r * Math.sin(phi) * Math.sin(theta),
|
||||
z: r * Math.cos(phi)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 生成随机方向向量
|
||||
* @en Generate random direction vector
|
||||
*/
|
||||
nextDirection2D(): { x: number; y: number } {
|
||||
const theta = this.next() * 2 * Math.PI;
|
||||
return {
|
||||
x: Math.cos(theta),
|
||||
y: Math.sin(theta)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建种子随机数生成器
|
||||
* @en Create seeded random number generator
|
||||
*/
|
||||
export function createSeededRandom(seed?: number): SeededRandom {
|
||||
return new SeededRandom(seed);
|
||||
}
|
||||
193
packages/framework/procgen/src/random/Shuffle.ts
Normal file
193
packages/framework/procgen/src/random/Shuffle.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* @zh 洗牌和采样工具
|
||||
* @en Shuffle and Sampling Utilities
|
||||
*/
|
||||
|
||||
import type { SeededRandom } from './SeededRandom';
|
||||
|
||||
/**
|
||||
* @zh Fisher-Yates 洗牌算法(原地修改)
|
||||
* @en Fisher-Yates shuffle algorithm (in-place)
|
||||
*
|
||||
* @param array - @zh 要洗牌的数组 @en Array to shuffle
|
||||
* @param rng - @zh 随机数生成器 @en Random number generator
|
||||
* @returns @zh 洗牌后的数组(同一数组)@en Shuffled array (same array)
|
||||
*/
|
||||
export function shuffle<T>(array: T[], rng: SeededRandom | { next(): number }): T[] {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(rng.next() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建洗牌后的副本(不修改原数组)
|
||||
* @en Create shuffled copy (does not modify original)
|
||||
*
|
||||
* @param array - @zh 原数组 @en Original array
|
||||
* @param rng - @zh 随机数生成器 @en Random number generator
|
||||
* @returns @zh 洗牌后的新数组 @en New shuffled array
|
||||
*/
|
||||
export function shuffleCopy<T>(array: readonly T[], rng: SeededRandom | { next(): number }): T[] {
|
||||
return shuffle([...array], rng);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从数组中随机选择一个元素
|
||||
* @en Pick a random element from array
|
||||
*
|
||||
* @param array - @zh 数组 @en Array
|
||||
* @param rng - @zh 随机数生成器 @en Random number generator
|
||||
* @returns @zh 随机元素 @en Random element
|
||||
*/
|
||||
export function pickOne<T>(array: readonly T[], rng: SeededRandom | { next(): number }): T {
|
||||
if (array.length === 0) {
|
||||
throw new Error('Cannot pick from empty array');
|
||||
}
|
||||
return array[Math.floor(rng.next() * array.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从数组中随机采样 N 个不重复元素
|
||||
* @en Sample N unique elements from array
|
||||
*
|
||||
* @param array - @zh 数组 @en Array
|
||||
* @param count - @zh 采样数量 @en Sample count
|
||||
* @param rng - @zh 随机数生成器 @en Random number generator
|
||||
* @returns @zh 采样结果 @en Sample result
|
||||
*/
|
||||
export function sample<T>(array: readonly T[], count: number, rng: SeededRandom | { next(): number }): T[] {
|
||||
if (count > array.length) {
|
||||
throw new Error('Sample count exceeds array length');
|
||||
}
|
||||
|
||||
if (count === array.length) {
|
||||
return shuffleCopy(array, rng);
|
||||
}
|
||||
|
||||
// For small sample sizes relative to array, use selection
|
||||
if (count < array.length / 2) {
|
||||
const result: T[] = [];
|
||||
const indices = new Set<number>();
|
||||
|
||||
while (result.length < count) {
|
||||
const index = Math.floor(rng.next() * array.length);
|
||||
if (!indices.has(index)) {
|
||||
indices.add(index);
|
||||
result.push(array[index]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// For large sample sizes, shuffle and take first N
|
||||
return shuffleCopy(array, rng).slice(0, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从数组中随机采样 N 个元素(可重复)
|
||||
* @en Sample N elements from array (with replacement)
|
||||
*
|
||||
* @param array - @zh 数组 @en Array
|
||||
* @param count - @zh 采样数量 @en Sample count
|
||||
* @param rng - @zh 随机数生成器 @en Random number generator
|
||||
* @returns @zh 采样结果 @en Sample result
|
||||
*/
|
||||
export function sampleWithReplacement<T>(
|
||||
array: readonly T[],
|
||||
count: number,
|
||||
rng: SeededRandom | { next(): number }
|
||||
): T[] {
|
||||
if (array.length === 0) {
|
||||
throw new Error('Cannot sample from empty array');
|
||||
}
|
||||
|
||||
const result: T[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(pickOne(array, rng));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 生成范围内的随机整数数组(不重复)
|
||||
* @en Generate array of random unique integers in range
|
||||
*
|
||||
* @param min - @zh 最小值(包含)@en Minimum (inclusive)
|
||||
* @param max - @zh 最大值(包含)@en Maximum (inclusive)
|
||||
* @param count - @zh 数量 @en Count
|
||||
* @param rng - @zh 随机数生成器 @en Random number generator
|
||||
* @returns @zh 随机整数数组 @en Array of random integers
|
||||
*/
|
||||
export function randomIntegers(
|
||||
min: number,
|
||||
max: number,
|
||||
count: number,
|
||||
rng: SeededRandom | { next(): number }
|
||||
): number[] {
|
||||
const range = max - min + 1;
|
||||
if (count > range) {
|
||||
throw new Error('Count exceeds range');
|
||||
}
|
||||
|
||||
// Generate all numbers and sample
|
||||
const numbers: number[] = [];
|
||||
for (let i = min; i <= max; i++) {
|
||||
numbers.push(i);
|
||||
}
|
||||
|
||||
return sample(numbers, count, rng);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 按权重从数组中采样(不重复)
|
||||
* @en Sample from array by weight (without replacement)
|
||||
*
|
||||
* @param items - @zh 项目数组 @en Item array
|
||||
* @param weights - @zh 权重数组 @en Weight array
|
||||
* @param count - @zh 采样数量 @en Sample count
|
||||
* @param rng - @zh 随机数生成器 @en Random number generator
|
||||
*/
|
||||
export function weightedSample<T>(
|
||||
items: readonly T[],
|
||||
weights: readonly number[],
|
||||
count: number,
|
||||
rng: SeededRandom | { next(): number }
|
||||
): T[] {
|
||||
if (items.length !== weights.length) {
|
||||
throw new Error('Items and weights must have same length');
|
||||
}
|
||||
if (count > items.length) {
|
||||
throw new Error('Sample count exceeds array length');
|
||||
}
|
||||
|
||||
const result: T[] = [];
|
||||
const remaining = [...items];
|
||||
const remainingWeights = [...weights];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let totalWeight = 0;
|
||||
for (const w of remainingWeights) {
|
||||
totalWeight += w;
|
||||
}
|
||||
|
||||
let r = rng.next() * totalWeight;
|
||||
let selectedIndex = 0;
|
||||
|
||||
for (let j = 0; j < remainingWeights.length; j++) {
|
||||
r -= remainingWeights[j];
|
||||
if (r <= 0) {
|
||||
selectedIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(remaining[selectedIndex]);
|
||||
remaining.splice(selectedIndex, 1);
|
||||
remainingWeights.splice(selectedIndex, 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
177
packages/framework/procgen/src/random/WeightedRandom.ts
Normal file
177
packages/framework/procgen/src/random/WeightedRandom.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @zh 加权随机工具
|
||||
* @en Weighted Random Utilities
|
||||
*/
|
||||
|
||||
import type { SeededRandom } from './SeededRandom';
|
||||
|
||||
/**
|
||||
* @zh 加权项
|
||||
* @en Weighted item
|
||||
*/
|
||||
export interface WeightedItem<T> {
|
||||
/**
|
||||
* @zh 项目值
|
||||
* @en Item value
|
||||
*/
|
||||
value: T;
|
||||
|
||||
/**
|
||||
* @zh 权重(> 0)
|
||||
* @en Weight (> 0)
|
||||
*/
|
||||
weight: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加权随机选择器
|
||||
* @en Weighted random selector
|
||||
*/
|
||||
export class WeightedRandom<T> {
|
||||
private readonly _items: WeightedItem<T>[];
|
||||
private readonly _cumulativeWeights: number[];
|
||||
private readonly _totalWeight: number;
|
||||
|
||||
/**
|
||||
* @zh 创建加权随机选择器
|
||||
* @en Create weighted random selector
|
||||
*
|
||||
* @param items - @zh 加权项数组 @en Array of weighted items
|
||||
*/
|
||||
constructor(items: WeightedItem<T>[]) {
|
||||
if (items.length === 0) {
|
||||
throw new Error('Items array cannot be empty');
|
||||
}
|
||||
|
||||
this._items = [...items];
|
||||
this._cumulativeWeights = [];
|
||||
|
||||
let total = 0;
|
||||
for (const item of this._items) {
|
||||
if (item.weight <= 0) {
|
||||
throw new Error('Weights must be positive');
|
||||
}
|
||||
total += item.weight;
|
||||
this._cumulativeWeights.push(total);
|
||||
}
|
||||
|
||||
this._totalWeight = total;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 随机选择一个项目
|
||||
* @en Randomly select an item
|
||||
*
|
||||
* @param rng - @zh 随机数生成器 @en Random number generator
|
||||
*/
|
||||
pick(rng: SeededRandom | { next(): number }): T {
|
||||
const r = rng.next() * this._totalWeight;
|
||||
|
||||
// Binary search for the selected item
|
||||
let left = 0;
|
||||
let right = this._cumulativeWeights.length - 1;
|
||||
|
||||
while (left < right) {
|
||||
const mid = (left + right) >>> 1;
|
||||
if (this._cumulativeWeights[mid] < r) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return this._items[left].value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 使用 Math.random 选择
|
||||
* @en Pick using Math.random
|
||||
*/
|
||||
pickRandom(): T {
|
||||
return this.pick({ next: () => Math.random() });
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取项目的选中概率
|
||||
* @en Get selection probability of an item
|
||||
*/
|
||||
getProbability(index: number): number {
|
||||
if (index < 0 || index >= this._items.length) {
|
||||
throw new Error('Index out of bounds');
|
||||
}
|
||||
return this._items[index].weight / this._totalWeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有项目数量
|
||||
* @en Get total item count
|
||||
*/
|
||||
get size(): number {
|
||||
return this._items.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取总权重
|
||||
* @en Get total weight
|
||||
*/
|
||||
get totalWeight(): number {
|
||||
return this._totalWeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从加权数组中随机选择
|
||||
* @en Pick from weighted array
|
||||
*
|
||||
* @param items - @zh 加权项数组 @en Array of weighted items
|
||||
* @param rng - @zh 随机数生成器 @en Random number generator
|
||||
*/
|
||||
export function weightedPick<T>(
|
||||
items: WeightedItem<T>[],
|
||||
rng: SeededRandom | { next(): number }
|
||||
): T {
|
||||
if (items.length === 0) {
|
||||
throw new Error('Items array cannot be empty');
|
||||
}
|
||||
|
||||
let totalWeight = 0;
|
||||
for (const item of items) {
|
||||
totalWeight += item.weight;
|
||||
}
|
||||
|
||||
let r = rng.next() * totalWeight;
|
||||
for (const item of items) {
|
||||
r -= item.weight;
|
||||
if (r <= 0) {
|
||||
return item.value;
|
||||
}
|
||||
}
|
||||
|
||||
return items[items.length - 1].value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从权重映射中随机选择
|
||||
* @en Pick from weight map
|
||||
*
|
||||
* @param weights - @zh 值到权重的映射 @en Map of values to weights
|
||||
* @param rng - @zh 随机数生成器 @en Random number generator
|
||||
*/
|
||||
export function weightedPickFromMap<T extends string | number>(
|
||||
weights: Record<T, number>,
|
||||
rng: SeededRandom | { next(): number }
|
||||
): T {
|
||||
const items: WeightedItem<T>[] = [];
|
||||
for (const key in weights) {
|
||||
items.push({ value: key as T, weight: weights[key] });
|
||||
}
|
||||
return weightedPick(items, rng);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建加权随机选择器
|
||||
* @en Create weighted random selector
|
||||
*/
|
||||
export function createWeightedRandom<T>(items: WeightedItem<T>[]): WeightedRandom<T> {
|
||||
return new WeightedRandom(items);
|
||||
}
|
||||
17
packages/framework/procgen/src/random/index.ts
Normal file
17
packages/framework/procgen/src/random/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @zh 随机工具模块
|
||||
* @en Random Utilities Module
|
||||
*/
|
||||
|
||||
export { SeededRandom, createSeededRandom } from './SeededRandom';
|
||||
export { WeightedRandom, weightedPick, weightedPickFromMap, createWeightedRandom } from './WeightedRandom';
|
||||
export type { WeightedItem } from './WeightedRandom';
|
||||
export {
|
||||
shuffle,
|
||||
shuffleCopy,
|
||||
pickOne,
|
||||
sample,
|
||||
sampleWithReplacement,
|
||||
randomIntegers,
|
||||
weightedSample
|
||||
} from './Shuffle';
|
||||
22
packages/framework/procgen/tsconfig.build.json
Normal file
22
packages/framework/procgen/tsconfig.build.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
23
packages/framework/procgen/tsconfig.json
Normal file
23
packages/framework/procgen/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../framework/core"
|
||||
},
|
||||
{
|
||||
"path": "../../framework/blueprint"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
packages/framework/procgen/tsup.config.ts
Normal file
12
packages/framework/procgen/tsup.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
treeshake: true,
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
Reference in New Issue
Block a user