Skip to main content

font_example/
font-example.rs

1use mittens_engine::{engine, utils};
2
3use mittens_engine::engine::ecs::component::{
4    AmbientLightComponent, BackgroundColorComponent, BackgroundComponent, Camera3DComponent,
5    CameraXRComponent, ColorComponent, InputComponent, InputTransformModeComponent,
6    InputXRComponent, PointerComponent, RaycastableComponent, TextComponent, TextShadowComponent,
7    TextureComponent, TextureFilteringComponent, TransformComponent, TransparentCutoutComponent,
8};
9
10#[path = "example_util/mod.rs"]
11mod example_util;
12
13fn main() {
14    mittens_engine::example_support::ensure_model_assets();
15    utils::logger::init();
16
17    let world = engine::ecs::World::default();
18    let mut universe = engine::Universe::new(world);
19
20    // Dark background so the font texture pops.
21    let background = universe
22        .world
23        .add_component(BackgroundColorComponent::new());
24    let background_c = universe
25        .world
26        .add_component(ColorComponent::rgba(0.20, 0.2, 0.20, 1.0));
27    let _ = universe.world.add_child(background, background_c);
28    universe.add(background);
29
30    // Ambient so text is readable even without explicit lights.
31    let ambient = universe
32        .world
33        .add_component(AmbientLightComponent::rgb(0.50, 0.50, 0.50));
34    universe.add(ambient);
35
36    let directional_tx = universe
37        .world
38        .add_component(TransformComponent::new().with_position(0.0, 0.5, 1.0));
39    let directional_light = universe.world.add_component(
40        engine::ecs::component::DirectionalLightComponent::new()
41            .with_color(1.0, 1.0, 1.0)
42            .with_intensity(0.8),
43    );
44    let _ = universe.attach(directional_tx, directional_light);
45    universe.add(directional_tx);
46
47    // --- background clouds ---
48    // Background stage (occluded + lit) so the cloud volume self-occludes but won't occlude
49    // the foreground text (renderer clears depth before foreground).
50    let bg_root = universe
51        .world
52        .add_component(BackgroundComponent::new().with_occlusion_and_lighting());
53    universe.add(bg_root);
54
55    let mut bg_cloud_params = example_util::CloudRingParams::default();
56    bg_cloud_params.cloud_count = 6;
57    bg_cloud_params.seed = 0xF0_17_C10u32;
58    example_util::spawn_cloud_ring(&mut universe, bg_root, bg_cloud_params);
59
60    // I {
61    //   // not fps rotation, just relative rotation
62    //   with_forward_z()
63    //   with_roll_axis_y()
64    //   C3D {}
65    // }
66    let input = universe
67        .world
68        .add_component(InputComponent::new().with_speed(2.0));
69    let input_mode = universe
70        .world
71        .add_component(InputTransformModeComponent::forward_z().with_roll_axis_y());
72    let _ = universe.attach(input, input_mode);
73
74    let rig_transform = universe
75        .world
76        .add_component(TransformComponent::new().with_position(1.8, -0.5, 2.5));
77    let _ = universe.attach(input, rig_transform);
78
79    let camera = universe.world.add_component(Camera3DComponent::new());
80    let _ = universe.attach(rig_transform, camera);
81
82    // Click-to-pick: treat this camera rig as a pointer source.
83    let pointer = universe.world.add_component(PointerComponent::new());
84    let _ = universe.attach(camera, pointer);
85
86    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
87    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
88
89    universe.add(input);
90
91    // --- foreground clouds ---
92    // Normal foreground renderables (not background stage).
93    // Offset the ring forward (negative Z) so several clusters are in view.
94    let fg_cloud_root = universe
95        .world
96        .add_component(TransformComponent::new().with_position(0.0, -6.0, -10.0));
97    universe.add(fg_cloud_root);
98
99    let mut fg_cloud_params = example_util::CloudRingParams::default();
100    fg_cloud_params.cloud_count = 4;
101    fg_cloud_params.radius = 9.0;
102    fg_cloud_params.center_y = 1.0;
103    fg_cloud_params.puffs_per_cloud = 22;
104    fg_cloud_params.angle_jitter = 0.35;
105    fg_cloud_params.high_y_probability = 0.35;
106    fg_cloud_params.high_y_multiplier = 1.4;
107    fg_cloud_params.seed = 0xF0_17_C102u32;
108    example_util::spawn_cloud_ring(&mut universe, fg_cloud_root, fg_cloud_params);
109
110    // T {
111    //   with_translation(0,0, -2)
112    //   TXT {
113    //     "ababaabbaabbaaabbbaaabbbaaaabbbbaaaaabbbbbababababa"
114    //     TextureComponent { assets/images/test.font_system.png }
115    //   }
116    // }
117    fn estimate_text_height_world(text: &str, scale: f32) -> f32 {
118        let line_count = text.lines().count().max(1) as f32;
119        // Text quads are ~1 unit tall per line in text-space.
120        // Add some padding so blocks don't feel cramped.
121        let pad_lines = 1.25;
122        (line_count + pad_lines) * scale
123    }
124
125    fn spawn_text_block(
126        universe: &mut engine::Universe,
127        position: (f32, f32, f32),
128        scale: f32,
129        text: &str,
130        font_texture_uri: Option<&str>,
131        text_color_rgba: Option<[f32; 4]>,
132        shadow: Option<TextShadowComponent>,
133        filtering: TextureFilteringComponent,
134    ) -> f32 {
135        // T_root { T_scale { [Color] { TXT { shadow, filtering } } } }
136        let text_root = universe.world.add_component(
137            TransformComponent::new().with_position(position.0, position.1, position.2),
138        );
139
140        let text_scale = universe
141            .world
142            .add_component(TransformComponent::new().with_scale(scale, scale, 1.0));
143        let _ = universe.attach(text_root, text_scale);
144
145        let text_parent = if let Some([r, g, b, a]) = text_color_rgba {
146            let color = universe
147                .world
148                .add_component(ColorComponent::rgba(r, g, b, a));
149            let _ = universe.attach(text_scale, color);
150            color
151        } else {
152            text_scale
153        };
154
155        let text_c = universe.world.add_component(TextComponent::new(text));
156        let _ = universe.attach(text_parent, text_c);
157
158        // Explicit opt-in: make the glyph renderables pickable.
159        // TextSystem will propagate this to all spawned glyph quads.
160        let raycastable = universe
161            .world
162            .add_component(RaycastableComponent::enabled());
163        let _ = universe.attach(text_c, raycastable);
164
165        // Route glyph quads into the alpha-to-coverage cutout pass.
166        let cutout = universe
167            .world
168            .add_component(TransparentCutoutComponent::new());
169        let _ = universe.attach(text_c, cutout);
170
171        if let Some(shadow) = shadow {
172            let shadow_id = universe.world.add_component(shadow);
173            let _ = universe.attach(text_c, shadow_id);
174        }
175
176        // Optional: override the font atlas for this text block.
177        // TextSystem will propagate this to all glyph renderables.
178        if let Some(uri) = font_texture_uri {
179            let tex = universe
180                .world
181                .add_component(TextureComponent::with_uri(uri));
182            let _ = universe.attach(text_c, tex);
183        }
184
185        let filtering_id = universe.world.add_component(filtering);
186        let _ = universe.attach(text_c, filtering_id);
187
188        universe.add(text_root);
189
190        estimate_text_height_world(text, scale)
191    }
192
193    // --- text blocks ---
194    // Multi-line samples at different scales.
195    // (Text literals omitted in the README/snippet; see constants below.)
196    const TEXT_BIG: &str = "CAT ENGINE\nfont example\nBIG TEXT";
197    const TEXT_MED: &str = "multi-line\ntext block\nmedium";
198    const TEXT_SMALL: &str = "small\ntext";
199    const TEXT_TINY: &str = "tiny\ntext\n(zoom in)";
200
201    // Stack vertically; advance by (measured height + gap) so big text gets more room.
202    let x = -1.2;
203    let z = -2.0;
204    let mut y = 1.2;
205    let gap = 0.15;
206
207    let shadow_crisp = Some(
208        TextShadowComponent::new()
209            .with_scale(1.35)
210            .with_offset([0.06, -0.06, 0.0015]),
211    );
212
213    y -= spawn_text_block(
214        &mut universe,
215        (x, y, z),
216        0.55,
217        TEXT_BIG,
218        None,
219        Some([1.0, 1.0, 1.0, 1.0]),
220        shadow_crisp,
221        TextureFilteringComponent::nearest_magnification(),
222    ) + gap;
223    y -= spawn_text_block(
224        &mut universe,
225        (x, y, z),
226        0.25,
227        TEXT_MED,
228        None,
229        Some([0.60, 0.95, 1.00, 1.0]),
230        Some(
231            TextShadowComponent::new()
232                .with_rgba([0.0, 0.0, 0.15, 1.0])
233                .with_scale(1.20)
234                .with_offset([0.05, -0.04, 0.0015]),
235        ),
236        TextureFilteringComponent::linear(),
237    ) + gap;
238    y -= spawn_text_block(
239        &mut universe,
240        (x, y, z),
241        0.14,
242        TEXT_SMALL,
243        None,
244        Some([1.0, 0.88, 0.35, 1.0]),
245        Some(
246            TextShadowComponent::new()
247                .with_rgba([0.15, 0.0, 0.0, 1.0])
248                .with_scale(1.55)
249                .with_offset([0.08, -0.08, 0.0015]),
250        ),
251        TextureFilteringComponent::nearest(),
252    ) + gap;
253    let _ = spawn_text_block(
254        &mut universe,
255        (x, y, z),
256        0.08,
257        TEXT_TINY,
258        None,
259        Some([0.90, 1.0, 0.70, 1.0]),
260        shadow_crisp,
261        TextureFilteringComponent::nearest_magnification(),
262    );
263
264    // Left block: explicit multi-line text using the default font_system atlas.
265    const TEXT_LEFT: &str = "even though there's hexes\nto the solar plexus in my lexus\ni'm feelin' reckless,\nwhen i'm eating breakfast";
266    let _ = spawn_text_block(
267        &mut universe,
268        (x - 8.1, 1.1, z),
269        0.22,
270        TEXT_LEFT,
271        Some("assets/textures/font_system.dds"),
272        Some([0.95, 0.95, 0.95, 1.0]),
273        Some(
274            TextShadowComponent::new()
275                .with_scale(1.25)
276                .with_offset([0.05, -0.05, 0.0015]),
277        ),
278        TextureFilteringComponent::nearest_magnification(),
279    );
280
281    // Alt atlas: put it *behind* the original stack (slightly farther from the camera)
282    // and tint it dark grey.
283    let alt_atlas = Some("assets/textures/font_system.0.0.dds");
284    let alt_z = z - 0.05;
285    let alt_grey = Some([0.25, 0.25, 0.25, 1.0]);
286
287    let mut y_alt = 1.2;
288    y_alt -= spawn_text_block(
289        &mut universe,
290        (x, y_alt, alt_z),
291        0.55,
292        TEXT_BIG,
293        alt_atlas,
294        alt_grey,
295        Some(
296            TextShadowComponent::new()
297                .with_rgba([0.0, 0.0, 0.0, 1.0])
298                .with_scale(1.15)
299                .with_offset([0.03, -0.03, 0.0015]),
300        ),
301        TextureFilteringComponent::linear(),
302    ) + gap;
303    y_alt -= spawn_text_block(
304        &mut universe,
305        (x, y_alt, alt_z),
306        0.25,
307        TEXT_MED,
308        alt_atlas,
309        alt_grey,
310        Some(
311            TextShadowComponent::new()
312                .with_rgba([0.0, 0.0, 0.0, 1.0])
313                .with_scale(1.15)
314                .with_offset([0.03, -0.03, 0.0015]),
315        ),
316        TextureFilteringComponent::linear(),
317    ) + gap;
318    y_alt -= spawn_text_block(
319        &mut universe,
320        (x, y_alt, alt_z),
321        0.14,
322        TEXT_SMALL,
323        alt_atlas,
324        alt_grey,
325        Some(
326            TextShadowComponent::new()
327                .with_rgba([0.0, 0.0, 0.0, 1.0])
328                .with_scale(1.15)
329                .with_offset([0.03, -0.03, 0.0015]),
330        ),
331        TextureFilteringComponent::linear(),
332    ) + gap;
333    let _ = spawn_text_block(
334        &mut universe,
335        (x, y_alt, alt_z),
336        0.08,
337        TEXT_TINY,
338        alt_atlas,
339        alt_grey,
340        Some(
341            TextShadowComponent::new()
342                .with_rgba([0.0, 0.0, 0.0, 1.0])
343                .with_scale(1.15)
344                .with_offset([0.03, -0.03, 0.0015]),
345        ),
346        TextureFilteringComponent::linear(),
347    );
348
349    universe.enable_repl();
350
351    let xr_input = universe.world.add_component(InputXRComponent::on());
352    let xr_gamepad = universe
353        .world
354        .add_component(engine::ecs::component::InputXRGamepadComponent::new().speed(1.5));
355    let xr_head = universe.world.add_component(TransformComponent::new());
356    let xr_camera = universe.world.add_component(CameraXRComponent::on());
357    let _ = universe.attach(xr_input, xr_head);
358    let _ = universe.attach(xr_input, xr_gamepad);
359    let _ = universe.attach(xr_head, xr_camera);
360    universe.add(xr_input);
361
362    // Add an OpenXR component so OpenXRSystem initializes and starts polling events.
363    let xr_root = universe
364        .world
365        .add_component(engine::ecs::component::XrComponent::on());
366    universe.add(xr_root);
367
368    // Process init-time registrations (Text expands into glyph subtrees here).
369    universe.systems.process_commands(
370        &mut universe.world,
371        &mut universe.visuals,
372        &mut universe.render_assets,
373        &mut universe.command_queue,
374    );
375
376    engine::Windowing::run_app(universe).expect("Windowing failed");
377}