案例展示
程序化生成城市、建筑、Loft 放样几何。
TIP
仅 WebGPU 模式可用(
renderer: 'webgpu')。
<template>
<div style="position: relative">
<div id="TO"></div>
<el-form id="formGen" label-width="80px">
<el-form-item label="类型:">
<el-select v-model="type" size="small">
<el-option label="程序化城市" value="city"></el-option>
<el-option label="程序化建筑" value="building"></el-option>
<el-option label="Loft 放样" value="loft"></el-option>
</el-select>
</el-form-item>
<template v-if="type === 'city'">
<el-form-item label="区块X:">
<div style="display: flex; align-items: center; width: 100%">
<el-slider
v-model="blocksX"
:min="1"
:max="5"
:step="1"
style="flex: 1; margin-right: 5px" />
<span class="pos-value">{{ blocksX }}</span>
</div>
</el-form-item>
<el-form-item label="区块Z:">
<div style="display: flex; align-items: center; width: 100%">
<el-slider
v-model="blocksZ"
:min="1"
:max="5"
:step="1"
style="flex: 1; margin-right: 5px" />
<span class="pos-value">{{ blocksZ }}</span>
</div>
</el-form-item>
</template>
<template v-if="type === 'building'">
<el-form-item label="宽度:">
<div style="display: flex; align-items: center; width: 100%">
<el-slider
v-model="width"
:min="4"
:max="24"
:step="1"
style="flex: 1; margin-right: 5px" />
<span class="pos-value">{{ width }}</span>
</div>
</el-form-item>
<el-form-item label="深度:">
<div style="display: flex; align-items: center; width: 100%">
<el-slider
v-model="depth"
:min="4"
:max="24"
:step="1"
style="flex: 1; margin-right: 5px" />
<span class="pos-value">{{ depth }}</span>
</div>
</el-form-item>
<el-form-item label="高度:">
<div style="display: flex; align-items: center; width: 100%">
<el-slider
v-model="height"
:min="10"
:max="80"
:step="1"
style="flex: 1; margin-right: 5px" />
<span class="pos-value">{{ height }}</span>
</div>
</el-form-item>
</template>
<template v-if="type === 'loft'">
<el-form-item label="形状:">
<el-select v-model="loftShape" size="small">
<el-option label="螺旋管" value="helix"></el-option>
<el-option label="扭曲方管" value="twist"></el-option>
<el-option label="花瓶" value="vase"></el-option>
</el-select>
</el-form-item>
<el-form-item label="颜色:">
<el-select v-model="loftColor" size="small">
<el-option label="蓝" value="#6c9bcf"></el-option>
<el-option label="绿" value="#57a359"></el-option>
<el-option label="橙" value="#e6a23c"></el-option>
<el-option label="粉" value="#f759ab"></el-option>
</el-select>
</el-form-item>
</template>
<el-form-item v-if="type !== 'loft'" label="种子:">
<div style="display: flex; align-items: center; width: 100%">
<el-slider
v-model="seed"
:min="1"
:max="999"
:step="1"
style="flex: 1; margin-right: 5px" />
<span class="pos-value">{{ seed }}</span>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="generate()" size="small">生成</el-button>
<el-button @click="clear()" size="small">清除</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElForm, ElFormItem, ElSelect, ElOption, ElSlider, ElButton } from 'element-plus'
let TO = null
let currentLoft = null
const type = ref('city')
const blocksX = ref(3)
const blocksZ = ref(3)
const width = ref(12)
const depth = ref(12)
const height = ref(40)
const seed = ref(1)
const loftShape = ref('helix')
const loftColor = ref('#6c9bcf')
onMounted(() => {
TO = new ThingOrigin('generatorScene', document.getElementById('TO'), {
scene: {
background: {
type: 'color',
color: { alpha: 1, color: '#999999' }
}
},
camera: {
position: { x: 0, y: 50, z: 200 }
},
lights: [
{ name: 'ambient', type: 'AmbientLight', color: '#ffffff', intensity: 0.7, visible: true },
{
name: 'dir',
type: 'DirectionalLight',
color: '#ffffff',
intensity: 0.8,
visible: true,
position: { x: 20, y: 40, z: 20 }
}
],
helper: {
axes: { active: false },
grid: { active: false }
}
})
generate()
})
// 生成
function generate() {
clear()
if (type.value === 'city') {
TO.generator.createCity({ seed: seed.value, blocksX: blocksX.value, blocksZ: blocksZ.value })
setCamera(0, 60, 500)
} else if (type.value === 'building') {
TO.generator.createBuilding({
seed: seed.value,
width: width.value,
depth: depth.value,
height: height.value
})
setCamera(0, height.value * 0.8, height.value * 1.6)
} else if (type.value === 'loft') {
const sections = makeSections(loftShape.value)
const loft = TO.generator.createLoft({
sections,
color: loftColor.value,
capStart: true,
capEnd: true,
position: { x: 0, y: 0, z: 0 }
})
TO.scene.add(loft)
currentLoft = loft
setCamera(0, 45, 170)
}
}
// 清除
function clear() {
TO.generator.removeCity()
TO.generator.removeBuilding()
if (currentLoft) {
TO.scene.remove(currentLoft)
currentLoft = null
}
}
// 设置相机
function setCamera(x, y, z) {
const cam = TO.camera.camera
cam.position.set(x, y, z)
cam.lookAt(0, 0, 0)
if (TO.controls.orbit) {
TO.controls.orbit.target.set(0, 0, 0)
TO.controls.orbit.update()
}
}
// 生成 Loft 截面(Vector3[][])
function makeSections(shape) {
const v = (x, y, z) => TO.tool.initVector3(x, y, z)
const sections = []
if (shape === 'helix') {
const N = 40
for (let i = 0; i <= N; i++) {
const t = (i / N) * Math.PI * 4
const cx = Math.sin(t) * 15
const cy = i * 2
const cz = Math.cos(t) * 15
const ring = []
for (let k = 0; k < 16; k++) {
const a = (k / 16) * Math.PI * 2
ring.push(v(cx + Math.cos(a) * 5, cy, cz + Math.sin(a) * 5))
}
sections.push(ring)
}
} else if (shape === 'twist') {
const N = 30
for (let i = 0; i <= N; i++) {
const z = i * 2 - 30
const ang = i * 0.3
const s = 8
const ring = []
for (let k = 0; k < 4; k++) {
const a = ang + (k * Math.PI) / 2
ring.push(v(Math.cos(a) * s, Math.sin(a) * s, z))
}
sections.push(ring)
}
} else if (shape === 'vase') {
const N = 24
for (let i = 0; i <= N; i++) {
const t = i / N
const y = i * 3
const r = 4 + 10 * Math.sin(t * Math.PI)
const ring = []
for (let k = 0; k < 24; k++) {
const a = (k / 24) * Math.PI * 2
ring.push(v(Math.cos(a) * r, y, Math.sin(a) * r))
}
sections.push(ring)
}
}
return sections
}
</script>
<style scoped>
#TO {
width: 100%;
height: 400px;
position: relative;
}
#formGen {
position: absolute;
top: 10px;
right: 10px;
min-width: 260px;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
z-index: 1000;
background: rgba(0, 0, 0, 0.6);
color: #fff;
padding: 12px 16px;
border-radius: 4px;
font-size: 12px;
}
.pos-value {
min-width: 50px;
text-align: right;
font-size: 12px;
color: #fff;
}
#formGen :deep(.el-form-item__label) {
color: #fff;
font-size: 12px;
}
</style>API 介绍
generator.createCity
| 方法签名 | 返回值 | 描述 |
|---|---|---|
createCity(options: CityOptions = {}) | Group | 程序化生成城市,并自动加入场景 |
CityOptions 参数说明:
| 参数名 | 说明 | 类型 | 必填 | 默认值 |
|---|---|---|---|---|
seed | 随机种子 | number | 否 | 1 |
blocksX | X 方向区块数 | number | 否 | 3 |
blocksZ | Z 方向区块数 | number | 否 | 3 |
generator.createBuilding
| 方法签名 | 返回值 | 描述 |
|---|---|---|
createBuilding(options: BuildingOptions = {}) | Group | 程序化生成单栋建筑,并自动加入场景 |
BuildingOptions 参数说明:
| 参数名 | 说明 | 类型 | 必填 | 默认值 |
|---|---|---|---|---|
seed | 随机种子 | number | 否 | Date.now() |
width | 占地宽 | number | 否 | 12 |
depth | 占地深 | number | 否 | 12 |
height | 总高度 | number | 否 | 40 |
generator.createLoft
| 方法签名 | 返回值 | 描述 |
|---|---|---|
createLoft(options: LoftOptions) | Object3D | 按截面序列放样生成几何体(需自行 TO.scene.add 加入场景) |
LoftOptions 参数说明:
| 参数名 | 说明 | 类型 | 必填 | 默认值 |
|---|---|---|---|---|
sections | 截面序列,每段为 Vector3[] 多边形 | Vector3[][] | 是 | - |
capStart | 封闭起点 | boolean | 否 | true |
capEnd | 封闭终点 | boolean | 否 | true |
color | 材质颜色 | string | 否 | #6c9bcf |
position | 位置 | { x, y, z } | 否 | { x:0, y:5, z:0 } |
modelName | 模型名 | string | 否 | loft-${Date.now()} |
Vector3可用TO.tool.initVector3(x, y, z)创建。