Tutorials
This page provides step-by-step tutorials for common OMath use cases.
Tutorial 1: Basic Vector Math
Learn the fundamentals of vector operations in OMath.
Step 1: Include OMath
#include <omath/omath.hpp>
#include <iostream>
using namespace omath;
Step 2: Create Vectors
// 2D vectors
Vector2<float> v2a{3.0f, 4.0f};
Vector2<float> v2b{1.0f, 2.0f};
// 3D vectors
Vector3<float> v3a{1.0f, 2.0f, 3.0f};
Vector3<float> v3b{4.0f, 5.0f, 6.0f};
// 4D vectors (often used for homogeneous coordinates)
Vector4<float> v4{1.0f, 2.0f, 3.0f, 1.0f};
Step 3: Perform Operations
// Addition
auto sum = v3a + v3b; // {5, 7, 9}
// Subtraction
auto diff = v3a - v3b; // {-3, -3, -3}
// Scalar multiplication
auto scaled = v3a * 2.0f; // {2, 4, 6}
// Dot product
float dot = v3a.dot(v3b); // 32.0
// Cross product (3D only)
auto cross = v3a.cross(v3b); // {-3, 6, -3}
// Length
float len = v3a.length(); // ~3.74
// Normalization (safe - returns original if length is zero)
auto normalized = v3a.normalized();
// Distance between vectors
float dist = v3a.distance_to(v3b); // ~5.196
Step 4: Angle Calculations
if (auto angle = v3a.angle_between(v3b)) {
std::cout << "Angle in degrees: " << angle->as_degrees() << "\n";
std::cout << "Angle in radians: " << angle->as_radians() << "\n";
} else {
std::cout << "Cannot compute angle (zero-length vector)\n";
}
// Check if perpendicular
if (v3a.is_perpendicular(v3b)) {
std::cout << "Vectors are perpendicular\n";
}
Key takeaways:
- All vector operations are type-safe and constexpr-friendly
- Safe normalization never produces NaN
- Angle calculations use std::expected for error handling
Tutorial 2: World-to-Screen Projection
Project 3D coordinates to 2D screen space for overlays and ESP.
Step 1: Choose Your Game Engine
#include <omath/omath.hpp>
// For Source Engine games (CS:GO, TF2, etc.)
using namespace omath::source_engine;
// Or for other engines:
// using namespace omath::unity_engine;
// using namespace omath::unreal_engine;
// using namespace omath::frostbite_engine;
Step 2: Set Up the Camera
using namespace omath;
using namespace omath::projection;
// Define viewport (screen dimensions)
ViewPort viewport{1920.0f, 1080.0f};
// Define field of view
auto fov = FieldOfView::from_degrees(90.0f);
// Camera position and angles
Vector3<float> camera_pos{0.0f, 0.0f, 100.0f};
ViewAngles camera_angles{
PitchAngle::from_degrees(0.0f),
YawAngle::from_degrees(0.0f),
RollAngle::from_degrees(0.0f)
};
// Create camera (using Source Engine in this example)
Camera camera(
camera_pos,
camera_angles,
viewport,
fov,
0.1f, // near plane
1000.0f // far plane
);
Step 3: Project 3D Points
// 3D world position (e.g., enemy player position)
Vector3<float> enemy_pos{150.0f, 200.0f, 75.0f};
// Project to screen
if (auto screen = camera.world_to_screen(enemy_pos)) {
std::cout << "Enemy on screen at: "
<< screen->x << ", " << screen->y << "\n";
// Draw ESP box or marker at screen->x, screen->y
// Note: screen coordinates are in viewport space (0-width, 0-height)
} else {
// Enemy is not visible (behind camera or outside frustum)
std::cout << "Enemy not visible\n";
}
Step 4: Update Camera for Each Frame
void render_frame() {
// Read current camera data from game
Vector3<float> new_pos = read_camera_position();
ViewAngles new_angles = read_camera_angles();
// Update camera
camera.update(new_pos, new_angles);
// Project all entities
for (const auto& entity : entities) {
if (auto screen = camera.world_to_screen(entity.position)) {
draw_esp_box(screen->x, screen->y);
}
}
}
Key takeaways:
- Choose the engine trait that matches your target game
- world_to_screen() returns std::optional - always check the result
- Update camera each frame for accurate projections
- Screen coordinates are in the viewport space you defined
Tutorial 3: Projectile Prediction (Aim-Bot)
Calculate where to aim to hit a moving target.
Step 1: Define Projectile Properties
#include <omath/omath.hpp>
#include <omath/projectile_prediction/proj_pred_engine_legacy.hpp>
using namespace omath;
using namespace omath::projectile_prediction;
// Define your weapon's projectile
Projectile bullet;
bullet.origin = Vector3<float>{0, 0, 0}; // Shooter position
bullet.speed = 800.0f; // Muzzle velocity (m/s or game units/s)
bullet.gravity = Vector3<float>{0, 0, -9.81f}; // Gravity vector
Step 2: Define Target State
// Target information (enemy player)
Target enemy;
enemy.position = Vector3<float>{100, 200, 50}; // Current position
enemy.velocity = Vector3<float>{10, 5, 0}; // Current velocity
Step 3: Calculate Aim Point
// Create prediction engine
// Use AVX2 version if available for better performance:
// ProjPredEngineAVX2 engine;
ProjPredEngineLegacy engine;
// Calculate where to aim
if (auto aim_point = engine.maybe_calculate_aim_point(bullet, enemy)) {
std::cout << "Aim at: "
<< aim_point->x << ", "
<< aim_point->y << ", "
<< aim_point->z << "\n";
// Calculate angles to aim_point
Vector3<float> aim_direction = (*aim_point - bullet.origin).normalized();
// Convert to view angles (engine-specific)
// ViewAngles angles = calculate_angles_to_direction(aim_direction);
// set_aim_angles(angles);
} else {
// Cannot hit target (too fast, out of range, etc.)
std::cout << "Target cannot be hit\n";
}
Step 4: Handle Different Scenarios
// Stationary target
Target stationary;
stationary.position = Vector3<float>{100, 100, 100};
stationary.velocity = Vector3<float>{0, 0, 0};
// aim_point will equal position for stationary targets
// Fast-moving target
Target fast;
fast.position = Vector3<float>{100, 100, 100};
fast.velocity = Vector3<float>{50, 0, 0}; // Moving very fast
// May return nullopt if target is too fast
// Target at different heights
Target aerial;
aerial.position = Vector3<float>{100, 100, 200}; // High up
aerial.velocity = Vector3<float>{5, 5, -10}; // Falling
// Gravity will be factored into the calculation
Step 5: Performance Optimization
// For better performance on modern CPUs, use AVX2:
#include <omath/projectile_prediction/proj_pred_engine_avx2.hpp>
ProjPredEngineAVX2 fast_engine; // 2-4x faster than legacy
// Use the same way as legacy engine
if (auto aim = fast_engine.maybe_calculate_aim_point(bullet, enemy)) {
// Process aim point
}
Key takeaways:
- Always check if aim point exists before using
- Velocity must be in same units as position/speed
- Gravity vector points down (typically negative Z or Y depending on engine)
- Use AVX2 engine when possible for better performance
- Returns nullopt when target is unreachable
Tutorial 4: Collision Detection
Perform ray-casting and intersection tests.
Step 1: Ray-Plane Intersection
#include <omath/omath.hpp>
using namespace omath;
// Define a ground plane (Z=0, normal pointing up)
Plane ground{
Vector3<float>{0, 0, 0}, // Point on plane
Vector3<float>{0, 0, 1} // Normal vector (Z-up)
};
// Define a ray (e.g., looking downward from above)
Vector3<float> ray_origin{10, 20, 100};
Vector3<float> ray_direction{0, 0, -1}; // Pointing down
// Test intersection
if (auto hit = ground.intersects_ray(ray_origin, ray_direction)) {
std::cout << "Hit ground at: "
<< hit->x << ", " << hit->y << ", " << hit->z << "\n";
// Expected: (10, 20, 0)
} else {
std::cout << "Ray does not intersect plane\n";
}
Step 2: Distance to Plane
// Calculate signed distance from point to plane
Vector3<float> point{10, 20, 50};
float distance = ground.distance_to_point(point);
std::cout << "Distance to ground: " << distance << "\n";
// Expected: 50.0 (50 units above ground)
// Negative distance means point is below the plane
Vector3<float> below{10, 20, -5};
float dist_below = ground.distance_to_point(below);
// Expected: -5.0
Step 3: Axis-Aligned Bounding Box
#include <omath/3d_primitives/box.hpp>
// Create a bounding box
Box bbox{
Vector3<float>{0, 0, 0}, // Min corner
Vector3<float>{100, 100, 100} // Max corner
};
// Test if point is inside
Vector3<float> inside{50, 50, 50};
if (bbox.contains(inside)) {
std::cout << "Point is inside box\n";
}
Vector3<float> outside{150, 50, 50};
if (!bbox.contains(outside)) {
std::cout << "Point is outside box\n";
}
// Box-box intersection
Box other{
Vector3<float>{50, 50, 50},
Vector3<float>{150, 150, 150}
};
if (bbox.intersects(other)) {
std::cout << "Boxes overlap\n";
}
Step 4: Line Tracing
#include <omath/collision/line_tracer.hpp>
using namespace omath::collision;
// Ray-triangle intersection
Vector3<float> v0{0, 0, 0};
Vector3<float> v1{100, 0, 0};
Vector3<float> v2{0, 100, 0};
Vector3<float> ray_start{25, 25, 100};
Vector3<float> ray_dir{0, 0, -1};
LineTracer tracer;
if (auto hit = tracer.ray_triangle_intersect(ray_start, ray_dir, v0, v1, v2)) {
std::cout << "Hit triangle at: "
<< hit->point.x << ", "
<< hit->point.y << ", "
<< hit->point.z << "\n";
std::cout << "Hit distance: " << hit->distance << "\n";
std::cout << "Surface normal: "
<< hit->normal.x << ", "
<< hit->normal.y << ", "
<< hit->normal.z << "\n";
}
Key takeaways: - Plane normals should be unit vectors - Ray direction should typically be normalized - Signed distance indicates which side of plane a point is on - AABB tests are very fast for broad-phase collision detection - Line tracer provides hit point, distance, and surface normal
Tutorial 5: Pattern Scanning
Search for byte patterns in memory.
Step 1: Basic Pattern Scanning
#include <omath/utility/pattern_scan.hpp>
#include <vector>
using namespace omath;
// Memory to search (e.g., from a loaded module)
std::vector<uint8_t> memory = {
0x48, 0x8B, 0x05, 0xAA, 0xBB, 0xCC, 0xDD,
0x48, 0x85, 0xC0, 0x74, 0x10,
// ... more bytes
};
// Pattern with wildcards (?? = match any byte)
PatternView pattern{"48 8B 05 ?? ?? ?? ?? 48 85 C0"};
// Scan for pattern
if (auto result = pattern_scan(memory, pattern)) {
std::cout << "Pattern found at offset: " << result->offset << "\n";
// Extract wildcard values if needed
// result->wildcards contains the matched bytes at ?? positions
} else {
std::cout << "Pattern not found\n";
}
Step 2: PE File Scanning
#include <omath/utility/pe_pattern_scan.hpp>
// Scan a PE file (EXE or DLL)
PEPatternScanner scanner("game.exe");
PatternView pattern{"E8 ?? ?? ?? ?? 85 C0 75 ??"};
if (auto rva = scanner.scan_pattern(pattern)) {
std::cout << "Pattern found at RVA: 0x"
<< std::hex << *rva << std::dec << "\n";
// Convert RVA to absolute address if needed
uintptr_t base_address = get_module_base("game.exe");
uintptr_t absolute = base_address + *rva;
} else {
std::cout << "Pattern not found in PE file\n";
}
Step 3: Multiple Patterns
// Search for multiple patterns
std::vector<PatternView> patterns{
PatternView{"48 8B 05 ?? ?? ?? ??"},
PatternView{"E8 ?? ?? ?? ?? 85 C0"},
PatternView{"FF 15 ?? ?? ?? ?? 48 8B"}
};
for (size_t i = 0; i < patterns.size(); ++i) {
if (auto result = pattern_scan(memory, patterns[i])) {
std::cout << "Pattern " << i << " found at: "
<< result->offset << "\n";
}
}
Step 4: Pattern with Masks
// Alternative: use mask-based patterns
// Pattern: bytes to match
std::vector<uint8_t> pattern_bytes{0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00};
// Mask: 'x' = must match, '?' = wildcard
std::string mask{"xxx????"};
// Custom scan function
auto scan_with_mask = [&](const std::vector<uint8_t>& data) {
for (size_t i = 0; i < data.size() - pattern_bytes.size(); ++i) {
bool match = true;
for (size_t j = 0; j < pattern_bytes.size(); ++j) {
if (mask[j] == 'x' && data[i + j] != pattern_bytes[j]) {
match = false;
break;
}
}
if (match) return i;
}
return size_t(-1);
};
Key takeaways:
- Use ?? in pattern strings for wildcards
- PE scanner works with files and modules
- Pattern scanning is useful for finding functions, vtables, or data
- Always validate found addresses before use
- Patterns may have multiple matches - consider context
Tutorial 6: Angles and View Angles
Work with game camera angles properly.
Step 1: Understanding Angle Types
#include <omath/omath.hpp>
using namespace omath;
// Generic angle with custom range
auto angle1 = Angle<float, 0.0f, 360.0f>::from_degrees(45.0f);
auto angle2 = Angle<float, -180.0f, 180.0f>::from_degrees(270.0f);
// Specialized camera angles
auto pitch = PitchAngle::from_degrees(-10.0f); // Looking down
auto yaw = YawAngle::from_degrees(90.0f); // Looking right
auto roll = RollAngle::from_degrees(0.0f); // No tilt
Step 2: Angle Conversions
// Create from degrees
auto deg_angle = PitchAngle::from_degrees(45.0f);
// Get as radians
float radians = deg_angle.as_radians();
std::cout << "45° = " << radians << " radians\n";
// Get as degrees
float degrees = deg_angle.as_degrees();
std::cout << "Value: " << degrees << "°\n";
Step 3: View Angles (Camera)
// Pitch: vertical rotation (-89° to 89°)
// Yaw: horizontal rotation (-180° to 180°)
// Roll: camera tilt (-180° to 180°)
ViewAngles camera_angles{
PitchAngle::from_degrees(-15.0f), // Looking slightly down
YawAngle::from_degrees(45.0f), // Facing northeast
RollAngle::from_degrees(0.0f) // No tilt
};
// Access individual components
float pitch_val = camera_angles.pitch.as_degrees();
float yaw_val = camera_angles.yaw.as_degrees();
float roll_val = camera_angles.roll.as_degrees();
Step 4: Calculating Look-At Angles
using namespace omath::source_engine; // Or your game's engine
Vector3<float> camera_pos{0, 0, 100};
Vector3<float> target_pos{100, 100, 100};
// Calculate angles to look at target
ViewAngles look_at = CameraTrait::calc_look_at_angle(camera_pos, target_pos);
std::cout << "Pitch: " << look_at.pitch.as_degrees() << "°\n";
std::cout << "Yaw: " << look_at.yaw.as_degrees() << "°\n";
std::cout << "Roll: " << look_at.roll.as_degrees() << "°\n";
Step 5: Angle Arithmetic
// Angles support arithmetic with automatic normalization
auto angle1 = YawAngle::from_degrees(170.0f);
auto angle2 = YawAngle::from_degrees(20.0f);
// Addition (wraps around)
auto sum = angle1 + angle2; // 190° → normalized to -170°
// Subtraction
auto diff = angle2 - angle1; // -150°
// Scaling
auto scaled = angle1 * 2.0f;
Key takeaways: - Use specialized angle types for camera angles (PitchAngle, YawAngle, RollAngle) - Angles automatically normalize to their valid ranges - Each game engine may have different angle conventions - Use engine traits to calculate look-at angles correctly
Next Steps
Now that you've completed these tutorials, explore:
- API Overview - Complete API reference
- Engine Documentation - Engine-specific features
- Examples - More code examples
- Getting Started - Quick start guide
Last updated: 1 Nov 2025