Skip to content

案例展示

程序化生成城市、建筑、Loft 放样几何。

TIP

仅 WebGPU 模式可用(renderer: 'webgpu')。

类型:
区块X:
3
区块Z:
3
种子:
1
<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随机种子number1
blocksXX 方向区块数number3
blocksZZ 方向区块数number3

generator.createBuilding

方法签名返回值描述
createBuilding(options: BuildingOptions = {})Group程序化生成单栋建筑,并自动加入场景

BuildingOptions 参数说明:

参数名说明类型必填默认值
seed随机种子numberDate.now()
width占地宽number12
depth占地深number12
height总高度number40

generator.createLoft

方法签名返回值描述
createLoft(options: LoftOptions)Object3D按截面序列放样生成几何体(需自行 TO.scene.add 加入场景)

LoftOptions 参数说明:

参数名说明类型必填默认值
sections截面序列,每段为 Vector3[] 多边形Vector3[][]-
capStart封闭起点booleantrue
capEnd封闭终点booleantrue
color材质颜色string#6c9bcf
position位置{ x, y, z }{ x:0, y:5, z:0 }
modelName模型名stringloft-${Date.now()}

Vector3 可用 TO.tool.initVector3(x, y, z) 创建。