返回

Python Ursina引擎大量实体渲染优化:减少延迟技巧

python

Python Ursina 引擎中优化大量实体渲染,减少延迟

这问题挺常见的, Ursina 里一下子加载一大堆实体, 特别是像 Minecraft 那种, 很容易卡。做游戏嘛, 谁不想帧率高点呢?先说下你碰到的情况, 建了个 25x25 的世界,铺了 3 层方块,就有 1900 个方块了。现在就觉得卡, 以后方块多了肯定更卡。你想问能不能远处的实体不渲染, 或者暂时禁用, 等靠近了再显示。

问题分析:为什么会卡?

渲染过程吃性能. Ursina (或者说任何 3D 引擎)渲染物体,都需要做很多计算。这些计算包括:

  • 顶点处理: 每个方块都有顶点,引擎要知道这些顶点在屏幕上的位置。
  • 光栅化: 把 3D 物体变成屏幕上的像素。
  • 纹理映射: 把图片(纹理)贴到物体表面。
  • 光照计算: 计算光线对物体的影响。

方块越多,这些计算量越大。即便是做了视锥体裁剪(只渲染视野内的物体),如果视野内方块依然很多,帧率还是上不去。

解决方案:

咱们来逐个试试解决办法.

1. 距离判断禁用 (效果有限,但可改进)

你已经试过这个了, 就是算每个方块和玩家的距离, 太远了就 disable()。想法是对的, 但是直接这么搞, 反而更卡. 为啥呢? 因为你每帧都要算所有方块的距离! 这计算量也很大.

改进方案:

  1. 降低检查频率: 不用每帧都检查,可以隔几帧,甚至隔几秒检查一次。隔多久可以根据实际情况调,找到一个平衡点。
  2. 分组管理: 把世界分成大的区块 (Chunks), 比如 16x16x16 大小的。只检查区块和玩家的距离, 如果区块远了, 把整个区块的方块都禁用。
    这样可以大大减少距离计算次数。
  3. 分层处理:把游戏分成多个不同详细程度的模型,然后不同距离使用不同的模型。比如,在几千米的地方是一个简单几何模型,而几百米内的显示完整模型。

示例代码 (降低检查频率):

from ursina import *

app = Ursina()

# 假设的玩家和方块列表
player = Entity(model='cube', color=color.orange)
blocks = [Entity(model='cube', position=(x, y, z)) for x in range(25) for y in range(3) for z in range(25)]

check_interval = 30  # 每30帧检查一次
frame_count = 0

def update():
    global frame_count
    frame_count += 1

    if frame_count >= check_interval:
        frame_count = 0
        for block in blocks:
            if distance(player, block) > 10:
                block.disable()
            else:
                block.enable()
    #下面是让玩家可以移动
    if held_keys['a']:
        player.x -= time.dt * 5
    if held_keys['d']:
        player.x += time.dt * 5
    if held_keys['w']:
        player.z += time.dt * 5
    if held_keys['s']:
        player.z -= time.dt * 5
    if held_keys['space']:
        player.y += time.dt * 5
    if held_keys['shift']:
        player.y -= time.dt * 5
EditorCamera()
app.run()

示例代码 (区块管理):

from ursina import *

app = Ursina()

player = Entity(model='cube', color=color.orange)
blocks = []
chunk_size = 8
#生成8x3x8的世界, 每个区块为 8x8x8.
world_size_x = 8
world_size_y = 3
world_size_z = 8

chunks = {}  # 用字典存储区块, key 是区块的坐标 (x, y, z)

def create_chunk(x, y, z):
    chunk = []
    for cx in range(chunk_size):
        for cy in range(chunk_size):
            for cz in range(chunk_size):
                block = Entity(model='cube', position=(x + cx, y + cy, z + cz))
                chunk.append(block)
    chunks[(x, y, z)] = chunk

# 创建所有区块
for x in range(0,world_size_x*chunk_size,chunk_size):
    for y in range(0, world_size_y * chunk_size, chunk_size):
        for z in range(0,world_size_z*chunk_size,chunk_size):
            create_chunk(x, y, z)

check_interval = 60
frame_count = 0

def update():
    global frame_count
    frame_count += 1

    if frame_count >= check_interval:
        frame_count = 0
        for chunk_pos, chunk_blocks in chunks.items():
            chunk_center = Vec3(chunk_pos[0] + chunk_size / 2, chunk_pos[1] + chunk_size / 2, chunk_pos[2] + chunk_size / 2)
            if distance(player.position, chunk_center) > 20:  # 调整这个距离
                for block in chunk_blocks:
                    block.disable()
            else:
                for block in chunk_blocks:
                    block.enable()
    #控制移动
    if held_keys['a']:
        player.x -= time.dt * 5
    if held_keys['d']:
        player.x += time.dt * 5
    if held_keys['w']:
        player.z += time.dt * 5
    if held_keys['s']:
        player.z -= time.dt * 5
    if held_keys['space']:
        player.y += time.dt * 5
    if held_keys['shift']:
        player.y -= time.dt * 5

EditorCamera()
app.run()

2. 使用 combine() 合并网格

你说不能把网格拆回方块, 其实 combine() 方法有一些参数是可以控制合并后的行为的. 关键是 auto_destroykeep_origin 这两个参数.

原理:

  • combine(): 把多个实体的网格合并成一个,减少 draw call (绘制调用) 次数。
  • auto_destroy=False: 合并后,原来的实体不会被销毁, 还保留着.
  • keep_origin=True: 保持合并后网格的原点位置,方便后续操作.

步骤:

  1. 创建方块时,先正常创建。
  2. combine() 把一个区域内的方块 (比如一个 Chunk) 合并成一个大网格,auto_destroy 设为 False, keep_origin设为 True
  3. 要破坏方块时,找到对应的原始方块实体 (因为 auto_destroy=False,它们还在),把它从场景中移除。
  4. 重新 combine() 受影响区域的方块.

注意点:

需要自己记录每个方块和合并后的大网格的对应关系,才能在破坏时找到正确的方块。可以用字典来存储。

示例代码 (简化版,演示 combine 的用法):

from ursina import *

app = Ursina()

# 创建一些方块
blocks = []
for x in range(5):
    for y in range(3):
        for z in range(5):
            block = Entity(model='cube', position=(x, y, z))
            blocks.append(block)

# 合并方块
combined_mesh = Entity(model='cube')
combined_mesh.combine(blocks, auto_destroy=False, keep_origin=True)

# 破坏其中一个方块 (假设是坐标 (1, 1, 1) 的)
for b in blocks:
    if b.position == (1,1,1):
        b.disable()#这里也可以使用b.parent = None来将其暂时不挂载

#重新combine
combined_mesh2 = Entity(model='cube')
combined_mesh2.combine(blocks, auto_destroy=False, keep_origin=True)
combined_mesh.disable()#可以关掉旧的, 可以留到之后复用,减少对象创建的消耗.
EditorCamera()

app.run()

3. 实例化渲染 (Instancing)

这个厉害了! 如果你的方块长得都一样 (比如都是普通立方体), 强烈推荐用实例化.

原理:

实例化是一种特殊的渲染技术。 告诉显卡:"我要画 N 个长得一样的物体, 你只算一次, 然后复制 N 份就行了." 这样可以极大减少 CPU 和 GPU 的负担.

Ursina 中使用:

Ursina 封装了实例化, 用起来很简单. 就是把模型换成 'instanced_cube'

from ursina import *

app = Ursina()

# 注意这里模型是 'instanced_cube'
blocks = [Entity(model='instanced_cube', position=(x, y, z)) for x in range(25) for y in range(3) for z in range(25)]
camera.clip_plane_far = 25
player = Entity(model='cube', color = color.orange)
def input(key):
	if key == 'q':
		print("改变")
		for block in blocks:
			if block.position.y <= 1:
				block.color = color.random_color()
		#重新进行渲染
		Entity(model='instanced_cube').combine(blocks, auto_destroy=True)
EditorCamera()

app.run()

注意:

实例化渲染的物体, 修改颜色、纹理之类的属性会比较麻烦, 因为它们共享一份数据. 如果你的方块有不同颜色或纹理,需要一些额外的技巧.

实例化后的combine,修改颜色或者纹理需要重新执行一遍Entity(model='instanced_cube').combine(blocks, auto_destroy=True),否则会不生效. 这种方法的局限性较大。不适用于频繁修改颜色的方块.

进阶:自定义 Instanced 模型

可以不使用'instanced_cube',使用其他自定义的模型.

  1. 你需要用 Mesh() 类创建一个自定义网格。
  2. 在创建实体时, 使用add_instance(position) 方法添加实例,而不是直接指定 position

这样做的灵活度很高,你几乎可以用任何形状的模型做实例化渲染.

4. 使用着色器 (Shaders)

如果上面这些方法都不能满足你, 并且你对图形学比较了解, 还可以考虑用着色器。

着色器是直接跑在 GPU 上的小程序. 可以自己写着色器来实现一些特殊的渲染效果, 比如把很多方块的顶点数据一次性传给 GPU, 在着色器里计算每个方块的位置, 这样可以把大量的计算从 CPU 转移到 GPU.

但是注意了,这个门槛很高, 需要学习着色器语言 (比如 GLSL). Ursina 支持自定义着色器, 但这属于高级技巧, 这里就不展开讲了.

5. camera.clip_plane_far的补充

你设置了 camera.clip_plane_far = 10, 这是限制了渲染距离.
如果方块真的很少的话,可以增加一点, 或者改为通过代码控制,这样给玩家更多控制的余地.

总结:

选哪个方法,要根据你具体的需求和技术水平来定:

  • 简单场景: 分组 + 降低检查频率 + 适当调整 camera.clip_plane_far 应该够用了.
  • 方块都一样: 强烈推荐实例化渲染! 性能提升巨大。
  • 需要破坏/修改方块: combine() 结合分组管理, 自己实现方块的移除和更新。
  • 追求极致性能: 了解一下着色器。

上面提到的所有方法,建议你在项目里多尝试一下, 调一调参数, 才能找到最优解.