Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 69 additions & 5 deletions editor/VoxelEditor/VoxelEditorActions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using editor.Assets;
using editor.Assets.Types;
Expand All @@ -25,6 +27,17 @@ public static void Register() {

private static async void OnBakeCompleted(VoxelBakeCompleted completed) {
try {
if (completed.Unused.Count > 0) {
foreach (var unused in completed.Unused) {
var unusedPath = ProjectContext.Resolve(unused);
AssetDatabase.TryDelete(unusedPath);
AssetDatabase.TryDelete(unusedPath + ".meta");
}
AssetDatabase.RebuildAssetDatabase();
Events.Send(new ReloadAssetsManifest());
}
if (string.IsNullOrEmpty(completed.Path)) return;

var realPath = ProjectContext.Resolve(completed.Path);
var header = MetaFile.ReadHeader(realPath);
if (header is null) return;
Expand All @@ -44,17 +57,68 @@ private static async Task BakeAsync(HierarchyElement node) {
return;
}

var virtualPath = await App.Modals.ShowSaveFile($"{node.Name}_baked", ".tvox");
if (virtualPath is null) return;
CreateVoxelAsset(virtualPath);
Events.Send(new VoxelBake { Target = node.Uid, Path = virtualPath, Replace = true });
var chosen = await App.Modals.ShowSaveFile($"{node.Name}_baked", ".tnode");
if (chosen is null) return;

var folder = chosen.EndsWith(PrefabExtension, StringComparison.OrdinalIgnoreCase)
? chosen[..^PrefabExtension.Length]
: chosen;
var name = folder[(folder.LastIndexOf('/') + 1)..];
var prefabPath = $"{folder}/{name}{PrefabExtension}";

var bake = new VoxelBake { Target = node.Uid, Path = prefabPath, Replace = true };
var taken = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { name, "Model" };

bake.BasePath = $"{folder}/Model.tvox";
CreateVoxelFile(bake.BasePath);

// Every volume gets a model
void Walk(HierarchyElement parent) {
foreach (var child in parent.Children) {
if (child.Type is "toast::FillVolume" or "toast::VoxelMesh") {
var path = $"{folder}/{UniqueFileName(child.Name, taken)}.tvox";
CreateVoxelFile(path);
bake.Pieces.Add(new VoxelBakePiece { Uid = child.Uid, Path = path });
}
if (child.Type is "toast::VoxelGroup" or "toast::FillVolume" or "toast::VoxelMesh" or "toast::CarveVolume"
or "toast::PaintVolume") {
Walk(child);
}
}
}

Walk(node);

var prefabReal = ProjectContext.Resolve(prefabPath);
Directory.CreateDirectory(Path.GetDirectoryName(prefabReal)!);
File.WriteAllBytes(prefabReal, Array.Empty<byte>());
MetaFile.Write(prefabReal, new MetaHeader { Uid = UidGenerator.Generate(), Type = "node" });

AssetDatabase.RebuildAssetDatabase();
Events.Send(new ReloadAssetsManifest());
Events.Send(bake);
}

internal static void CreateVoxelAsset(string virtualPath) {
private const string PrefabExtension = ".tnode";

private static string UniqueFileName(string name, HashSet<string> taken) {
var invalid = Path.GetInvalidFileNameChars();
var clean = new string(name.Trim().Select(c => invalid.Contains(c) || c == ' ' ? '_' : c).ToArray());
if (clean.Length == 0) clean = "Piece";
var candidate = clean;
for (var i = 2; !taken.Add(candidate); i++) candidate = $"{clean}_{i}";
return candidate;
}

private static void CreateVoxelFile(string virtualPath) {
var realPath = ProjectContext.Resolve(virtualPath);
Directory.CreateDirectory(Path.GetDirectoryName(realPath)!);
File.WriteAllBytes(realPath, Array.Empty<byte>());
MetaFile.Write(realPath, new MetaHeader { Uid = UidGenerator.Generate(), Type = "voxel_model" });
}

internal static void CreateVoxelAsset(string virtualPath) {
CreateVoxelFile(virtualPath);
AssetDatabase.RebuildAssetDatabase();
Events.Send(new ReloadAssetsManifest());
}
Expand Down
12 changes: 11 additions & 1 deletion engine/assets/shaders/debris.slang
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ static const float kSleepSpeed = 0.2; // below this speed a piece may slee
static const uint kSleepSteps = 15;
static const float kBounceSpeed = 0.5; // slower impact sticks
static const float kContactSlop = 0.001; // next step does not start touching
static const float kFloorZ = 0.0f; // lowest height a piece reaches

// Graphics queue patches sections in place unordered with this queue so new records can still be uninitialized headroom
// No robust buffer access so every index is checked against the buffer
Expand Down Expand Up @@ -363,7 +364,7 @@ void integrateMain(uint3 id : SV_DispatchThreadID, uint3 groupThread : SV_GroupT

if ((particle.packed & kFlagAsleep) != 0u) {
// Wakes once nothing supports it
bool supported = false;
bool supported = particle.position.z <= kFloorZ + 2.0 * kContactSlop;
for (uint bit = 0u; bit < 8u && !supported; ++bit) {
if (((mask >> bit) & 1u) != 0u) {
float3 below = particle.position + rotateByQuat(particle.rotation, bitOffset(bit, motion.baseScale)) - float3(0.0, 0.0, gParams.voxelSize);
Expand All @@ -387,6 +388,15 @@ void integrateMain(uint3 id : SV_DispatchThreadID, uint3 groupThread : SV_GroupT
particle.velocity += gParams.gravity * dt;
particle.position += particle.velocity * dt;

// Nothing goes under the ground plane
if (particle.position.z < kFloorZ) {
particle.position.z = kFloorZ;
// It stops dead, otherwise it keeps spinning and sliding along the floor
particle.velocity = float3(0.0, 0.0, 0.0);
motion.angular = float3(0.0, 0.0, 0.0);
touching = true;
}

float spin = length(motion.angular);
if (spin > 1e-5) {
particle.rotation = normalize(particle.rotation + 0.5 * dt * quatMul(float4(motion.angular, 0.0), particle.rotation));
Expand Down
89 changes: 51 additions & 38 deletions engine/src/toast/physics/debris.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ struct DebrisCluster {
glm::ivec3 origin {0};
uint8_t mask = 0;
uint8_t material = 0;
bool whole = false;
glm::vec3 centre {0.0f};
float scale = 1.0f;
uint32_t voxel_count = 0;
};

struct DebrisSpawn {
Expand All @@ -56,6 +60,7 @@ struct DebrisBlast {
glm::vec3 start {};
glm::vec3 end {};
float energy = 0.0f;
float max_speed = 0.0f;
};

inline constexpr float k_max_debris_speed = 40.0f;
Expand All @@ -69,8 +74,35 @@ constexpr auto debrisBit(const glm::ivec3& local) -> uint32_t {
}

[[nodiscard]]
inline auto clusterDebris(std::span<const DebrisVoxel> voxels, const glm::ivec3& jitter, Rng& rng, float split_chance)
-> std::vector<DebrisCluster> {
inline auto wholeDebris(std::span<const DebrisVoxel> voxels) -> DebrisCluster {
DebrisCluster out;
out.whole = true;
glm::ivec3 low = voxels.front().voxel;
glm::ivec3 high = low;
std::array<uint32_t, 256> counts {};
for (const DebrisVoxel& v : voxels) {
low = glm::min(low, v.voxel);
high = glm::max(high, v.voxel);
++counts[v.material];
}

const glm::vec3 extent = glm::vec3(high - low + 1);
out.centre = (glm::vec3(low) + glm::vec3(high + 1)) * 0.5f;
out.scale = std::max({extent.x, extent.y, extent.z}) * 0.5f;
out.voxel_count = static_cast<uint32_t>(voxels.size());
out.material = static_cast<uint8_t>(std::ranges::max_element(counts) - counts.begin());

for (const DebrisVoxel& v : voxels) {
const glm::vec3 point = glm::vec3(v.voxel) + 0.5f;
const uint32_t bit =
(point.x >= out.centre.x ? 1u : 0u) | (point.y >= out.centre.y ? 2u : 0u) | (point.z >= out.centre.z ? 4u : 0u);
out.mask |= static_cast<uint8_t>(1u << bit);
}
return out;
}

[[nodiscard]]
inline auto clusterDebris(std::span<const DebrisVoxel> voxels, const glm::ivec3& jitter) -> std::vector<DebrisCluster> {
struct Entry {
glm::ivec3 cell;
uint32_t bit;
Expand Down Expand Up @@ -108,47 +140,28 @@ inline auto clusterDebris(std::span<const DebrisVoxel> voxels, const glm::ivec3&
++last;
}

// a block sometimes breaks into 2 or 3 smaller pieces
const auto count = static_cast<uint32_t>(std::popcount(present));
uint32_t parts = 1;
if (count > 1 && rng.chance(split_chance)) {
parts = (count > 2 && rng.chance(0.5f)) ? 3u : 2u;
}
std::array<uint32_t, 3> part_masks {};
uint8_t best = 0;
int best_count = 0;
for (uint32_t bit = 0; bit < 8; ++bit) {
if ((present & (1u << bit)) != 0) {
part_masks[parts == 1 ? 0 : rng.below(parts)] |= 1u << bit;
}
}

for (uint32_t p = 0; p < parts; ++p) {
if (part_masks[p] == 0) {
if ((present & (1u << bit)) == 0) {
continue;
}
// most common material of the part
uint8_t best = 0;
int best_count = 0;
for (uint32_t bit = 0; bit < 8; ++bit) {
if ((part_masks[p] & (1u << bit)) == 0) {
continue;
}
int same = 0;
for (uint32_t other = 0; other < 8; ++other) {
same += ((part_masks[p] & (1u << other)) != 0 && material[other] == material[bit]) ? 1 : 0;
}
if (same > best_count) {
best_count = same;
best = material[bit];
}
int same = 0;
for (uint32_t other = 0; other < 8; ++other) {
same += ((present & (1u << other)) != 0 && material[other] == material[bit]) ? 1 : 0;
}
if (same > best_count) {
best_count = same;
best = material[bit];
}
out.push_back(
DebrisCluster {
.origin = (entries[first].cell * 2) + jitter,
.mask = static_cast<uint8_t>(part_masks[p]),
.material = best,
}
);
}
out.push_back(
DebrisCluster {
.origin = (entries[first].cell * 2) + jitter,
.mask = static_cast<uint8_t>(present),
.material = best,
}
);
first = last;
}
return out;
Expand Down
3 changes: 3 additions & 0 deletions engine/src/toast/physics/fracture.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <toast/voxel/palette.hpp>
#include <toast/voxel/voxel_constants.hpp>
#include <toast/voxel/voxel_volume.hpp>
#include <tracy/Tracy.hpp>
#include <unordered_set>
#include <vector>

Expand Down Expand Up @@ -223,6 +224,7 @@ inline auto reachesThroughMaterial(const voxel::Volume& volume, const glm::vec3&
[[nodiscard]]
inline auto reachableMaterial(const voxel::Volume& volume, const glm::vec3& start, const glm::vec3& end, float radius)
-> std::unordered_set<uint64_t> {
ZoneScopedN("physics::ReachableMaterial");
std::unordered_set<uint64_t> out;

// voxel closest to the hit
Expand Down Expand Up @@ -461,6 +463,7 @@ inline auto planCrater(
const voxel::Volume& volume, const voxel::Palette& palette, const voxel::MaterialLibrary& materials,
const FractureRequest& request
) -> Crater {
ZoneScopedN("physics::PlanCrater");
Crater out {
.start = request.point,
.end = request.point,
Expand Down
2 changes: 1 addition & 1 deletion engine/src/toast/physics/fracture_random.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ inline auto valueNoise3(uint64_t seed, glm::vec3 p) noexcept -> float {
float x01 = LARP(at(0, 1, 0), at(1, 1, 0), w.x);
float x10 = LARP(at(0, 0, 1), at(1, 0, 1), w.x);
float x11 = LARP(at(0, 1, 1), at(1, 1, 1), w.x);
return LARP(LARP(x00, x10, w.y), LARP(x01, x11, w.y), w.z);
return LARP(LARP(x00, x01, w.y), LARP(x10, x11, w.y), w.z);

#undef LARP
}
Expand Down
51 changes: 47 additions & 4 deletions engine/src/toast/physics/physics_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,53 @@ void registerPhysicsSettings() {
.max = 8.0,
.step = 0.25},
&Tunables::explosion_falloff);
flt("physics.fracture.explosion_chunk_size",
1.0,
{.label = "Explosion chunk size",
.category = "Fracture",
.description = "Meters across the chunks an explosion breaks material into",
.min = 0.2,
.max = 4.0,
.step = 0.1},
&Tunables::explosion_chunk_size);
flt("physics.fracture.explosion_debris_ratio",
8.0,
{.label = "Explosion debris ratio",
.category = "Fracture",
.description = "Material hit by a blast this many times its toughness turns to debris instead of chunks",
.min = 1.0,
.max = 64.0,
.step = 0.5},
&Tunables::explosion_debris_ratio);
flt("physics.fracture.explosion_shatter_width",
0.6,
{.label = "Explosion shatter width",
.category = "Fracture",
.description = "Meters of cracked material around the hole an explosion leaves, 0 for none",
.min = 0.0,
.max = 4.0,
.step = 0.1},
&Tunables::explosion_shatter_width);
flt("physics.fracture.explosion_max_speed",
20.0,
{.label = "Explosion max speed",
.category = "Fracture",
.description = "Fastest an explosion throws a chunk, debris or body, in m/s. Faster tunnels through thin colliders",
.min = 1.0,
.max = 100.0,
.step = 1.0},
&Tunables::explosion_max_speed);
uint_setting(
"physics.fracture.explosion_max_fragments",
48,
{.label = "Explosion max fragments",
.category = "Fracture",
.description = "Chunks one explosion can promote to their own bodies per shape; the rest become debris",
.min = 0.0,
.max = 256.0,
.step = 1.0},
&Tunables::explosion_max_fragments
);
uint_setting(
"physics.fracture.impact_max_hardness",
1,
Expand All @@ -399,10 +446,6 @@ void registerPhysicsSettings() {
{.label = "Max unglue voxels", .category = "Fracture", .min = 0.0, .max = 1000000.0, .step = 64.0},
&Tunables::fracture_max_unglue_voxels
);
flt("physics.fracture.debris_split_chance",
0.35,
{.label = "Debris split chance", .category = "Shatter", .min = 0.0f, .max = 1.0f, .step = 0.05f},
&Tunables::fracture_debris_split_chance);
uint_setting(
"physics.fracture.max_shatter_cells",
128,
Expand Down
6 changes: 5 additions & 1 deletion engine/src/toast/physics/physics_settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ struct Tunables {
float fracture_max_overshoot = 2.0f;
float fracture_bond_cost = 1.0f;
uint32_t fracture_max_unglue_voxels = 4096;
float fracture_debris_split_chance = 0.35f;
uint32_t fracture_max_shatter_cells = 128;
float fracture_max_shatter_distance = 8.0f;
uint32_t debris_max_spawns_per_step = 512;
Expand All @@ -55,6 +54,11 @@ struct Tunables {
float debris_friction = 0.6f;
float explosion_max_radius = 8.0f;
float explosion_falloff = 2.0f;
float explosion_chunk_size = 1.0f;
float explosion_debris_ratio = 8.0f;
float explosion_shatter_width = 0.6f;
uint32_t explosion_max_fragments = 48;
float explosion_max_speed = 20.0f;
uint32_t impact_max_hardness = 1;
uint32_t min_bounds_per_job = 32;
uint32_t min_candidates_per_job = 2;
Expand Down
5 changes: 5 additions & 0 deletions engine/src/toast/physics/raycast.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,11 @@ auto Simulator::bodyFor(const toast::Node& node) -> BodyID {
return rigidbody->bodyID();
}
if (const toast::Box<toast::VoxelNode> voxel = current.as<toast::VoxelNode>(); voxel.exists()) {
for (const toast::VoxelPart& part : voxel->parts()) {
if (part.body != BodyID {}) {
return part.body;
}
}
return voxel->bodyID();
}
}
Expand Down
Loading
Loading