You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Bevy 0.10.1中如何渲染3D文本?支持macOS与WASM

针对Bevy 0.10.1的3D动态文本渲染方案

以下是适配你的需求(兼容macOS/WASM、可动态更新、作为3D实体子节点)的几种可行方案,按推荐程度排序:

方案1:使用bevy_prototype_text_mesh第三方crate(推荐)

这个crate专门为Bevy提供3D文本网格生成能力,直接与PbrBundle兼容,支持动态更新文本内容,且完美适配Bevy 0.10.1和目标平台。

步骤1:添加依赖

在Cargo.toml中加入对应版本的依赖:

[dependencies]
bevy = "0.10.1"
bevy_prototype_text_mesh = "0.10.0" # 与Bevy 0.10.x版本匹配

步骤2:初始化插件与字体资源

use bevy::prelude::*;
use bevy_prototype_text_mesh::*;

#[derive(Resource)]
struct GameFont(Handle<Font>);

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugin(TextMeshPlugin)
        .add_startup_system(load_game_font)
        .add_system(update_card_numbers)
        .run();
}

// 预加载游戏字体
fn load_game_font(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.insert_resource(GameFont(asset_server.load("fonts/your_font.ttf")));
}

步骤3:实现render_text函数

该函数会生成带自定义组件的文本实体,并挂载为卡牌的子节点:

#[derive(Component)]
struct CardNumberComponent {
    value: u32,
}

fn render_text(
    commands: &mut Commands,
    game_font: Res<GameFont>,
    parent_entity: Entity,
    initial_value: u32,
    transform: Transform,
) -> Entity {
    commands.spawn((
        TextMeshBundle {
            text_mesh: TextMesh::new(game_font.0.clone(), initial_value.to_string())
                .with_font_size(24.0)
                .with_color(Color::WHITE),
            transform,
            // 绑定PBR材质以支持3D光照
            pbr: PbrBundle {
                material: commands.spawn(MaterialBundle {
                    material: StandardMaterial {
                        base_color: Color::WHITE,
                        ..Default::default()
                    },
                    ..Default::default()
                }).id(),
                ..Default::default()
            },
            ..Default::default()
        },
        CardNumberComponent { value: initial_value },
    ))
    .set_parent(parent_entity)
    .id()
}

步骤4:动态更新文本

通过Query监听自定义组件的变化,实时更新文本内容:

fn update_card_numbers(mut query: Query<(&mut TextMesh, &CardNumberComponent)>) {
    for (mut text_mesh, number_comp) in query.iter_mut() {
        let target_text = number_comp.value.to_string();
        if text_mesh.text() != target_text {
            text_mesh.set_text(target_text);
        }
    }
}

方案2:运行时生成纹理绑定到PbrBundle

如果你不想依赖第三方crate,可以手动用rusttype生成文本纹理,直接通过Bevy的Assets<Image>管理,无需文件系统操作,兼容WASM。

步骤1:添加依赖

[dependencies]
bevy = "0.10.1"
rusttype = "0.9.2"

步骤2:实现文本纹理生成函数

use bevy::prelude::*;
use rusttype::{Font, Scale, point};

fn generate_text_image(text: &str, font_data: &[u8], font_size: f32, color: Color) -> Image {
    let font = Font::try_from_bytes(font_data).unwrap();
    let scale = Scale::uniform(font_size);
    let v_metrics = font.v_metrics(scale);
    
    // 计算文本边界
    let glyphs: Vec<_> = font.layout(text, scale, point(0.0, v_metrics.ascent)).collect();
    let width = glyphs.iter()
        .map(|g| g.position().x + g.unpositioned().h_metrics().advance_width)
        .max_by(|a, b| a.partial_cmp(b).unwrap())
        .unwrap_or(0.0) as u32;
    let height = (v_metrics.ascent.abs() + v_metrics.descent.abs()) as u32;

    // 生成RGBA纹理缓冲区
    let mut buffer = vec![0; (width * height * 4) as usize];
    for glyph in glyphs {
        if let Some(bb) = glyph.pixel_bounding_box() {
            glyph.draw(|x, y, alpha| {
                let x = x + bb.min.x as u32;
                let y = y + bb.min.y as u32;
                if x < width && y < height {
                    let idx = (y * width + x) * 4;
                    buffer[idx] = (color.r() * 255.0) as u8;
                    buffer[idx + 1] = (color.g() * 255.0) as u8;
                    buffer[idx + 2] = (color.b() * 255.0) as u8;
                    buffer[idx + 3] = (color.a() * alpha * 255.0) as u8;
                }
            });
        }
    }

    Image::new_fill(
        UVec2::new(width, height),
        ImagePixelFormat::Rgba8UnormSrgb,
        &buffer,
    )
}

步骤3:实现render_text与更新逻辑

#[derive(Component)]
struct CardNumberComponent {
    value: u32,
    current_text: String,
    font_handle: Handle<Font>,
}

fn render_text(
    commands: &mut Commands,
    asset_server: Res<AssetServer>,
    mut images: ResMut<Assets<Image>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    fonts: Res<Assets<Font>>,
    parent_entity: Entity,
    initial_value: u32,
    transform: Transform,
) -> Entity {
    let font = fonts.get(&asset_server.load("fonts/your_font.ttf")).unwrap();
    let initial_text = initial_value.to_string();
    let text_image = generate_text_image(&initial_text, font.data(), 24.0, Color::WHITE);
    
    let image_handle = images.add(text_image);
    let material_handle = materials.add(StandardMaterial {
        base_color_texture: Some(image_handle),
        alpha_mode: AlphaMode::Blend,
        ..Default::default()
    });

    commands.spawn((
        PbrBundle {
            mesh: commands.spawn(MeshBundle {
                mesh: shape::Quad::new(Vec2::new(1.0, 0.2)).into(), // 根据文本尺寸调整Quad大小
                transform: Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -90.0, 0.0, 0.0)),
                ..Default::default()
            }).id(),
            material: material_handle,
            transform,
            ..Default::default()
        },
        CardNumberComponent {
            value: initial_value,
            current_text: initial_text,
            font_handle: asset_server.load("fonts/your_font.ttf"),
        },
    ))
    .set_parent(parent_entity)
    .id()
}

// 动态更新文本纹理
fn update_card_numbers(
    mut query: Query<(&mut Handle<StandardMaterial>, &mut CardNumberComponent)>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    mut images: ResMut<Assets<Image>>,
    fonts: Res<Assets<Font>>,
) {
    for (mut mat_handle, mut number_comp) in query.iter_mut() {
        let target_text = number_comp.value.to_string();
        if number_comp.current_text != target_text {
            let font = fonts.get(&number_comp.font_handle).unwrap();
            let new_image = generate_text_image(&target_text, font.data(), 24.0, Color::WHITE);
            let new_image_handle = images.add(new_image);
            
            if let Some(material) = materials.get_mut(mat_handle.as_mut()) {
                material.base_color_texture = Some(new_image_handle);
            }
            number_comp.current_text = target_text;
        }
    }
}

方案3:优化预生成图片方案

如果坚持使用预生成的数字图片,可通过缓存图片Handle减少重复加载,同时确保WASM下资源路径正确(trunk serve会自动打包assets目录下的资源):

#[derive(Resource)]
struct NumberTextures(Vec<Handle<Image>>);

// 预加载所有数字纹理
fn load_number_textures(mut commands: Commands, asset_server: Res<AssetServer>) {
    let mut textures = Vec::new();
    for i in 0..10 {
        textures.push(asset_server.load(format!("cards/nums/number_{}.png", i)));
    }
    commands.insert_resource(NumberTextures(textures));
}

// 更新卡牌数字
fn update_card_numbers(
    mut query: Query<(&mut Handle<StandardMaterial>, &CardNumberComponent)>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    number_textures: Res<NumberTextures>,
) {
    for (mut mat_handle, number_comp) in query.iter_mut() {
        let texture_handle = &number_textures.0[number_comp.value as usize];
        if let Some(material) = materials.get_mut(mat_handle.as_mut()) {
            material.base_color_texture = Some(texture_handle.clone());
        }
    }
}

内容的提问来源于stack exchange,提问作者Smartguy 88

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 16:47:02