From 1b2e9c1258b41b4d71b0929b284df7bcc1394aec Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 16:37:59 +0200 Subject: [PATCH 01/49] Map: allocator translation cost is linear (core r2 probes, tolerance, chosen constants) Imagination pass, aetheria-release, session self-2026-10-09-morning. Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index b30ea7141..3cd61747c 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -7073,6 +7073,66 @@ That file's Cut 4 (AI heading planner) and Cut 5 (combat facing) are carried as allocator's input and rewrites throttle-lock's two mixer-shaped tests in allocator terms. One owner of `Ship.Update`'s thruster block at every step. +### Revision: translation error costs linearly (core r2) + +Pass: Imagination, session `self-2026-10-09-morning`, after Hands stopped on `cut-thrust-allocator-core.r1` +(question `allocator-translation-cost-l2-has-no-onset`, ruling `allocator-l1-translation-cost`, operator +ruling `turn-authority-full`). Probes ran the real `CultMath.BoundedLeastSquares` (CultLib `614fd445`, the +pinned 0.3.0) in a scratch console on Yggdrasil, logs `l1probe1.log` to `l1probe3.log` in the session +scratchpad (`imag-allocator-r2`). Column sets: TA3 (Duel Longinus); TA3 with the clockwise Talaria zeroed; +a symmetric Djinni-like hull (mains (0,50,+-20), fore laterals (+-20,0,+-80), aft laterals (+-20,0,-+60), +bow (0,-30,0)); drives 0.3% apart for the tiny-imbalance hold. + +- **TA10. A squared translation cost has no onset** (Hands' probe, confirmed): r1's TA-R2 mixes the + drive into a turn from Turn 0 for any weights, so it cannot meet `turn-authority-full`. +- **TA11. The split form works only when the solver resolves it.** Variables: throttles and four error + variables e+x, e-x, e+y, e-y in [0,2]; rows H*(B_r u / s_r - e+ + e-) = H d_r / s_r, the yaw row + Wy * B_z u / s_z = Wy d_z / s_z, a cost row w*e = -w*kappa per error variable (linear slope + L = 2 w^2 kappa, quadratic part at most e/(2 kappa) of it), a ridge row rho*u per throttle. + At the solver's fixed KKT tolerance (1e-6 of the gradient at the origin) every one of 84 settings in two + grids failed somewhere: Hands' (H 300, Wy 1, w .01, kappa 2, rho .01) and the first choice (H 100, Wy 10, + w .01, kappa 10, rho 1e-3) gave all-zero throttles for move (0,1) and for a strafe, because the cost + signal L is below the tolerance against the H^2 and Wy^2 terms; larger L settings served translation + but gave throttles that depended on the warm start by up to 0.62 (ties the ridge cannot resolve below + the tolerance). The chain the form needs, tolerance << rho^2 << L << H^2, Wy^2, with L/H^2 about 1e-4 + for a sharp onset and L/Wy^2 about 1e-6 for the hold, spans more than the 1e6 that tolerance leaves. +- **TA12. With the tolerance at 1e-12 everything holds** (`l1probe3.log`, same grid, scratch change of + the constant): warm-start difference 0 in every setting, IterationLimit never reached, at most 10 + iterations. Chosen H 100, Wy 1000, w .3, kappa 10, rho .1: + + | Check | Result | + |---|---| + | Duel onset, clockwise / counter-clockwise | .772 / .691 (columns: 166.26/215.50 = .7715, 124.63/180.49 = .6905) | + | Duel, clockwise Talaria lost | drive from Turn > 0, Turn .1 gives drive .0999; Turn 1 gives 49.23 deg/s | + | Symmetric hull | no translation up to Turn .88 (pure-torque pairs, then mains cancelled by the bow); strafe .95 of the extreme | + | Hold, forward, yaw rate (deg/s) | intact -.0003, strong drive off .0003, weak drive off -.0002, drives .3% apart .0003 | + | Forward served, move (0,1) | 1.000 of the forward extreme | + | Largest throttle step per .001 of Turn over [-1,1] | .0133 (the steepest column ratio; Duel's drive slope is .0044) | + | Largest yaw residual in the sweeps | .0006 deg/s | + | cond(A), estimated from A^T A | 1.0e4; the solver documents exact results through 3e5 | + + Neighbouring settings fail one check each: H 30 leaks the drive at Turn .001; w 1 or 3 leaks it at + .0014 throttle before the onset (L/H^2 too large); rho .03 makes throttles jump up to .12 per .001 of + Turn (near-ties resolved too steeply); Wy 300 lets the hold drift .003 deg/s. +- **TA13. The Duel Longinus does not strafe.** Under the linear cost a starboard strafe would need the + counter-turning drive at full to hold heading, which costs more forward error than the strafe gains, + so move (1,0) gives no throttle. Today's mixer gives none either (TA4). The symmetric hull strafes. + +Rationale changes: + +- **The solve keeps one call; the tolerance is the solver owner's to expose.** CultMath's + `BoundedLeastSquares.Solve` gets an optional relative KKT tolerance (default unchanged, 1e-6), filled in + CultLib, never worked around in Aetheria; the allocator passes 1e-12 and relies on the solver's + progress rule to stop at its noise floor. Follow-up `cultmath-bls-kkt-tolerance` carries it; Aetheria + takes it by a pin bump in the core cut's first commit. +- **The onset is an ordering, not a label.** Under a linear cost the actuator with the least normalised + translation per unit of turn serves first and the next joins only when it saturates; on the Duel that + is the Talaria, so the onset is the Talaria's share of the turn extreme and moves with the columns + (damage, heat, brownout). Nothing classifies a thruster as attitude or drive. +- The bullets "One solve with a large yaw weight" and "The ridge" above describe r1 and are superseded + by this revision: the yaw weight is 1000 against a ridge of .1, and uniqueness of the answer depends on + the tolerance as well as the ridge. + ## Spec refresh 2026-10-09: scarce consumables, the fog bank, multiply, tiered boosters, the shipped AetherDb Pass: Imagination `imagination-spec-refresh`, session `self-2026-10-09-morning`, follow-up From 739eee4a8ed63deb5f2c9f92b67ef6ea3f0f267d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:17:36 +0200 Subject: [PATCH 02/49] Consumable host: tests first (red at the base; they build, three fail) Adds AConsumableModifierBoostsUntilExpiry, ABoughtConsumableActivates and ANonStackableConsumableRefusesASecondActivation. This commit builds; its tests fail until the host cut lands. Co-Authored-By: Claude Opus 5.5 --- .../Aetheria.Shared.Tests/ConsumableTests.cs | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 tests/Aetheria.Shared.Tests/ConsumableTests.cs diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs new file mode 100644 index 000000000..3fdd6aad3 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using CultMath; +using GameCult.Caching; +using Xunit; + +// A consumable is a cargo item with a timed effect (aetheria-release CS-R1). Its modifiers exist in the resolver +// exactly while its effect is active: the host (Entity) initialises the effect's behaviours at activation and +// disposes them at expiry, and ItemManager.CreateInstance is the one way a lot becomes a ConsumableItem. +public sealed class ConsumableTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-consumable-" + Guid.NewGuid().ToString("N")); + private string Catalog => Path.Combine(_root, "Aetheria.cc"); + private static readonly int2 EngineCell = new int2(0, 0); + private static readonly int2 HoldCell = new int2(1, 1); + + public ConsumableTests() => Directory.CreateDirectory(_root); + public void Dispose() => Directory.Delete(_root, true); + + private sealed class Rig + { + public CultCache Cache; + public ItemManager Items; + public Ship Ship; + public EquippedItem Engine; + public PerformanceStat Thrust; + public ConsumableItemData Consumable; + public CultRecordKey Key; + public CultRecordRef Maker; + + public ConsumableItem Mint() => Assert.IsType(Items.CreateInstance(Items.CreateLot(Consumable, Maker, .5f))); + public EquippedCargoBay Hold => Ship.CargoBays.Single(); + public int InCargo => Hold.Cargo.Keys.Count(i => i is ConsumableItem); + } + + // A ship with a thruster (Thrust 10) and a hold; the consumable doubles ThrusterData.Thrust for one second. + private Rig Build(bool stackable = false) + { + var thrust = new PerformanceStat { Min = 10, Max = 10 }; + var cache = AetheriaStores.Open(Catalog, catalogWritable: true); + cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); + cache.Upsert(new Faction { Name = "Maker", ShortName = "MKR" }); + var hullShape = new Shape(3, 3); + foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true; + var hullRef = cache.Upsert(new HullData + { + Name = "Skiff", HullType = HullType.Ship, Shape = hullShape, Durability = 10, + Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = EngineCell, Shape = new Shape() } } + }); + var engineRef = cache.Upsert(new GearData + { + Name = "Engine", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 10, + Behaviors = { new ThrusterData { Thrust = thrust } } + }); + var interior = new Shape(1, 1); + foreach (var cell in interior.AllCoordinates) interior[cell] = true; + var holdRef = cache.Upsert(new CargoBayData { Name = "Hold", Shape = new Shape(), InteriorShape = interior, Durability = 10 }); + var consumableRef = cache.Upsert(new ConsumableItemData + { + Name = "Overdrive", Duration = 1f, Stackable = stackable, Shape = new Shape(), + Behaviors = + { + new StatModifierData + { + Stat = new StatReference { Target = nameof(ThrusterData), Stat = nameof(ThrusterData.Thrust) }, + Modifier = new PerformanceStat { Min = 2, Max = 2 }, + Type = StatModifierType.Multiplier + } + } + }); + cache.FlushAsync().Wait(); + + var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { }); + var makerRef = cache.RefOf(cache.GetByName("Maker")); + EquippableItem Gear(EquippableItemData design) => (EquippableItem) items.CreateInstance(items.CreateLot(design, makerRef, .5f)); + var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null); + var ship = new Ship(items, zone, Gear(cache.Get(hullRef)), new EntitySettings()); + Assert.True(ship.TryEquip(Gear(cache.Get(engineRef)), EngineCell)); + Assert.True(ship.TryEquip(Gear(cache.Get(holdRef)), HoldCell)); + zone.Entities.Add(ship); + ship.Activate(); + var data = cache.Get(consumableRef); + return new Rig + { + Cache = cache, Items = items, Ship = ship, Thrust = thrust, Consumable = data, + Key = items.ItemData.RefOf(data).Key, + Engine = ship.Equipment.Single(e => e.Data.Name == "Engine"), + Maker = makerRef + }; + } + + // Two ticks after activation the modifier has attached (the first tick executes it, the second applies it). + private static void Tick(Rig rig, int times, float dt) + { + for (var i = 0; i < times; i++) rig.Ship.Update(dt); + } + + // The rule: a consumable's modifiers exist in the resolver exactly while its effect is active, never after. + // Observed at the resolved stat and at the resolver's modifier entry count, not at a flag. + [Fact] + public void AConsumableModifierBoostsUntilExpiry() + { + var rig = Build(); + using var _ = rig.Cache; + var resolver = rig.Ship.Resolver; + Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); + var baseline = resolver.ModifierEntryCount; + + rig.Ship.ActivateConsumable(rig.Mint()); + Tick(rig, 2, .3f); // 0.4s of the 1s remain; the modifier has attached + Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); + Assert.True(resolver.ModifierEntryCount > baseline); + + Tick(rig, 3, .3f); // 0.9s elapsed in all: expired on the third of these ticks + Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); + Assert.Equal(baseline, resolver.ModifierEntryCount); + } + + // A lot minted through the one production path is a ConsumableItem, and a hold holding it activates it. + [Fact] + public void ABoughtConsumableActivates() + { + var rig = Build(); + using var _ = rig.Cache; + Assert.True(rig.Hold.TryStore(rig.Mint())); + Assert.Equal(1, rig.InCargo); + + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.Equal(0, rig.InCargo); + Assert.NotNull(rig.Ship.FindActiveConsumable(rig.Consumable)); + } + + // CS-R1: a non-stackable design allows one active effect at a time, so its Duration is its cooldown. + [Fact] + public void ANonStackableConsumableRefusesASecondActivation() + { + var rig = Build(); + using var _ = rig.Cache; + Assert.True(rig.Hold.TryStore(rig.Mint())); + Assert.True(rig.Hold.TryStore(rig.Mint())); + + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.False(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.Equal(1, rig.InCargo); + + Tick(rig, 5, .3f); // the first effect has run out + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.Equal(0, rig.InCargo); + } +} From c6bca1a52234d737943f07b7e00f1da1b057d26f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:17:47 +0200 Subject: [PATCH 03/49] ThrottleLock behaviour: an active consumable forces full forward intent, read once in Ship.Update ThrottleLockData (union 41) marks the lock; Entity.ThrottleLocked is derived from the active effects; Ship.Update reads one `move` in place of MovementDirection. Turn is untouched (ruling overdrive-keep-steering). Not yet verified by a build when committed. Co-Authored-By: Claude Opus 5.5 --- .../ServerShared/Behaviors/Behaviors.cs | 1 + .../ServerShared/Behaviors/ThrottleLock.cs | 35 ++++ Assets/Scripts/ServerShared/Entity.cs | 3 + Assets/Scripts/ServerShared/Ship.cs | 14 +- .../ThrottleLockTests.cs | 174 ++++++++++++++++++ 5 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 Assets/Scripts/ServerShared/Behaviors/ThrottleLock.cs create mode 100644 tests/Aetheria.Shared.Tests/ThrottleLockTests.cs diff --git a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs index 9f2e292a6..4b07d7bb8 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs @@ -187,6 +187,7 @@ public interface IPowerConsumer // superseded draft quoted -- checked directly here rather than trusted from that document. Union(39, typeof(TargetingSystemData)), Union(40, typeof(MineLayerData)), +Union(41, typeof(ThrottleLockData)), JsonObject(MemberSerialization.OptIn)] public abstract class BehaviorData { diff --git a/Assets/Scripts/ServerShared/Behaviors/ThrottleLock.cs b/Assets/Scripts/ServerShared/Behaviors/ThrottleLock.cs new file mode 100644 index 000000000..d33ef07cb --- /dev/null +++ b/Assets/Scripts/ServerShared/Behaviors/ThrottleLock.cs @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using MessagePack; +using Newtonsoft.Json; + +// Presence marks the lock: Entity.ThrottleLocked is true while an active effect carries one, and Ship.Update +// then reads full forward intent in place of MovementDirection. The behaviour holds no state and does nothing +// itself; turning stays with the pilot. +[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable] +public class ThrottleLockData : BehaviorData +{ + public override Behavior CreateInstance(EquippedItem item) + { + return new ThrottleLock(this, item); + } + public override Behavior CreateInstance(ConsumableItemEffect item) + { + return new ThrottleLock(this, item); + } +} + +public class ThrottleLock : Behavior +{ + public ThrottleLock(ThrottleLockData data, EquippedItem item) : base(data, item) + { + } + + public ThrottleLock(ThrottleLockData data, ConsumableItemEffect item) : base(data, item) + { + } + + public override bool Execute(float dt) => true; +} diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 1da6e68d8..4855db616 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -863,6 +863,9 @@ public abstract class Entity _activeConsumables.Add(new ConsumableItemEffect(item, this)); } + // Derived, never written: true iff an active consumable effect carries a ThrottleLock. + public bool ThrottleLocked => _activeConsumables.Any(e => e.Behaviors.OfType().Any()); + public ConsumableItemEffect FindActiveConsumable(ConsumableItemData data) { return _activeConsumables.FirstOrDefault(ac => ac.Data == data); diff --git a/Assets/Scripts/ServerShared/Ship.cs b/Assets/Scripts/ServerShared/Ship.cs index 98dfa4fb3..993d92940 100644 --- a/Assets/Scripts/ServerShared/Ship.cs +++ b/Assets/Scripts/ServerShared/Ship.cs @@ -233,27 +233,29 @@ public class Ship : Entity if (_active && !_exitingWormhole && !_enteringWormhole) { RecalculateThrust(); + // The lock replaces intent here, once; player and agent intent both pass through this one read. + var move = ThrottleLocked ? float2(0, 1) : MovementDirection; foreach (var thruster in _allThrusters) thruster.Axis = 0; var rightThrusterTorqueCompensation = abs(RightStrafeTotalTorque) / RightStrafeTorqueThrusters.Count; foreach (var thruster in _rightThrusters) { var thrust = 0f; - thrust += MovementDirection.x; + thrust += move.x; if (RightStrafeTorqueThrusters.Contains(thruster)) - thrust -= MovementDirection.x * (rightThrusterTorqueCompensation / (abs(thruster.Torque) * thruster.Thrust)); + thrust -= move.x * (rightThrusterTorqueCompensation / (abs(thruster.Torque) * thruster.Thrust)); thruster.Axis = thrust; } var leftThrusterTorqueCompensation = abs(LeftStrafeTotalTorque) / LeftStrafeTorqueThrusters.Count; foreach (var thruster in _leftThrusters) { var thrust = 0f; - thrust += -MovementDirection.x; + thrust += -move.x; if (LeftStrafeTorqueThrusters.Contains(thruster)) - thrust += MovementDirection.x * (leftThrusterTorqueCompensation / (abs(thruster.Torque) * thruster.Thrust)); + thrust += move.x * (leftThrusterTorqueCompensation / (abs(thruster.Torque) * thruster.Thrust)); thruster.Axis = thrust; } - foreach (var thruster in _forwardThrusters) thruster.Axis += MovementDirection.y; - foreach (var thruster in _reverseThrusters) thruster.Axis += -MovementDirection.y; + foreach (var thruster in _forwardThrusters) thruster.Axis += move.y; + foreach (var thruster in _reverseThrusters) thruster.Axis += -move.y; foreach (var thruster in _clockwiseThrusters) thruster.Axis += Turn; foreach (var thruster in _counterClockwiseThrusters) thruster.Axis += -Turn; diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs new file mode 100644 index 000000000..1673e2be1 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using CultMath; +using GameCult.Caching; +using Xunit; +using static CultMath.math; + +// A ThrottleLock on an active consumable replaces the pilot's movement intent with full forward, read once at +// the top of Ship.Update's active branch. Turn is untouched. Every observation is a thruster's Axis after a real +// Ship.Update, with the effect started through Entity.ActivateConsumable. +public sealed class ThrottleLockTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-throttlelock-" + Guid.NewGuid().ToString("N")); + private string Catalog => Path.Combine(_root, "Aetheria.cc"); + + public ThrottleLockTests() => Directory.CreateDirectory(_root); + public void Dispose() => Directory.Delete(_root, true); + + private static GameplaySettings Settings() => new GameplaySettings + { + DefaultEntitySettings = new EntitySettings(), + Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } }, + QualityPriceModifier = new ExponentialLerp() + }; + + private static PerformanceStat Constant(float v) => new PerformanceStat { Min = v, Max = v }; + + private static EquippableItem Mint(CultCache cache, ItemManager items, ItemData design, ItemRotation rotation = ItemRotation.None) => new EquippableItem + { + Data = cache.RefOf(design), + Durability = design is EquippableItemData equippable ? equippable.Durability : 0, + Rotation = rotation, + Lot = items.Lots.Add(new Lot { Design = cache.RefOf(design), Origin = new Attributed(), Quality = .5f, Roles = new List() }) + }; + + private sealed class Fixture + { + public CultCache Cache; + public ItemManager Items; + public Ship Ship; + public ConsumableItemData Lock; + public Thruster Forward, Reverse, Right, Left, Clockwise; + + public void ActivateLock() => Ship.ActivateConsumable(new ConsumableItem + { + Data = Cache.RefOf(Lock), + Lot = Items.Lots.Add(new Lot { Design = Cache.RefOf(Lock), Origin = new Attributed(), Quality = .5f, Roles = new List() }) + }); + } + + // One thruster per intent direction, plus two reverse thrusters either side of the centre line, one of + // which has clockwise torque (the one Turn reaches). Thrusters are found by their item's rotation, as Ship + // sorts them. + private Fixture Build(float lockDuration) + { + var cache = AetheriaStores.Open(Catalog, catalogWritable: true); + cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); + var hullShape = new Shape(5, 5); + foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true; + cache.Upsert(new HullData { Name = "Skiff", HullType = HullType.Ship, Shape = hullShape, Durability = 10, Mass = 1000 }); + cache.Upsert(new GearData + { + Name = "Thruster", Hardpoint = HardpointType.Tool, Shape = new Shape(), Durability = 10, + MinimumTemperature = 0, MaximumTemperature = 1000, OptimalTemperature = 280, PlateauWidth = 400, + Behaviors = { new ThrusterData { Thrust = Constant(100), Visibility = Constant(0), Heat = Constant(0), EnergyUsage = Constant(0) } } + }); + var lockData = new ConsumableItemData { Name = "Overdrive", Duration = lockDuration, Behaviors = { new ThrottleLockData() } }; + cache.Upsert(lockData); + cache.FlushAsync().Wait(); + + var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); + var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null); + var ship = new Ship(items, zone, Mint(cache, items, cache.GetByName("Skiff")), new EntitySettings()); + var gear = cache.GetByName("Thruster"); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Reversed), new int2(2, 1))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(0, 2))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(4, 2))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(3, 3))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(1, 3))); + zone.Entities.Add(ship); + ship.Aim = float3(0, 0, 1); + ship.Activate(); + + var thrusters = ship.GetBehaviors().ToArray(); + return new Fixture + { + Cache = cache, Items = items, Ship = ship, Lock = cache.GetByName("Overdrive"), + Forward = thrusters.Single(t => t.Item.EquippableItem.Rotation == ItemRotation.Reversed), + Right = thrusters.Single(t => t.Item.EquippableItem.Rotation == ItemRotation.CounterClockwise), + Left = thrusters.Single(t => t.Item.EquippableItem.Rotation == ItemRotation.Clockwise), + Reverse = thrusters.First(t => t.Item.EquippableItem.Rotation == ItemRotation.None && t.Torque <= 0), + Clockwise = thrusters.First(t => t.Item.EquippableItem.Rotation == ItemRotation.None && t.Torque > 0) + }; + } + + // Spec ALockedThrottleIgnoresIntent. Mutation: read MovementDirection in place of move at any of the six + // sites in Ship.Update and the matching assertion goes red. + [Fact] + public void ALockedThrottleIgnoresIntent() + { + var f = Build(100f); + using var _ = f.Cache; + f.ActivateLock(); + Assert.True(f.Ship.ThrottleLocked); + + foreach (var intent in new[] { float2(0, -1), float2(1, 0), float2(-1, 0) }) + { + f.Ship.MovementDirection = intent; + f.Ship.Turn = 0; + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Forward.Axis); + Assert.Equal(0f, f.Reverse.Axis); + Assert.Equal(0f, f.Right.Axis); + Assert.Equal(0f, f.Left.Axis); + } + + // Turning stays with the pilot: the clockwise-torque thruster follows Turn while the lock holds. + f.Ship.MovementDirection = float2(0, -1); + f.Ship.Turn = 1; + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Clockwise.Axis); + f.Ship.Turn = 0; + f.Ship.Update(0.01f); + Assert.Equal(0f, f.Clockwise.Axis); + } + + // Spec TheLockEndsWithItsEffect. Mutation: make ThrottleLocked a latch (set on activation, never cleared) + // and the post-expiry reverse axis stays 0. + [Fact] + public void TheLockEndsWithItsEffect() + { + var f = Build(1f); + using var _ = f.Cache; + f.ActivateLock(); + f.Ship.MovementDirection = float2(0, -1); + f.Ship.Update(0.5f); + Assert.Equal(0f, f.Reverse.Axis); + + f.Ship.Update(2f); // runs past the Duration; the effect leaves at the end of this tick + Assert.False(f.Ship.ThrottleLocked); + + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Reverse.Axis); + Assert.Equal(0f, f.Forward.Axis); + } + + // Spec AnUnlockedShipFollowsIntent. Mutation: replace the conditional by an unconditional float2(0, 1) and + // the reverse and strafe assertions go red. + [Fact] + public void AnUnlockedShipFollowsIntent() + { + var f = Build(1f); + using var _ = f.Cache; + Assert.False(f.Ship.ThrottleLocked); + + f.Ship.MovementDirection = float2(0, -1); + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Reverse.Axis); + Assert.Equal(0f, f.Forward.Axis); + + f.Ship.MovementDirection = float2(1, 0); + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Right.Axis); + Assert.Equal(0f, f.Left.Axis); + Assert.Equal(0f, f.Forward.Axis); + + f.Ship.MovementDirection = float2(0, 1); + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Forward.Axis); + Assert.Equal(0f, f.Reverse.Axis); + } +} From 0e89dd16731b84a157bc071169bf65ddc00adfe6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:19:22 +0200 Subject: [PATCH 04/49] FireControl.RoundAt: the one statement of where a fired round is at a sim time PendingShot freezes the weapon's Velocity as Speed at Fire; RoundAt(shot, time) reads the frozen origin, direction, speed and fire time, clamped to the FireRange point. Step, Commit and Apply do not call it. BallisticFlightTests pin the line against the shooter position, aim and authored velocity. Builds: yes. Presenters still fly their own rounds at this commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/FireControl.cs | 13 ++ .../BallisticFlightTests.cs | 127 ++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 tests/Aetheria.Shared.Tests/BallisticFlightTests.cs diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index 138776ce9..097c3cc79 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -514,6 +514,16 @@ public static class FireControl return shot.Target == null ? 1f : PDeviation(deviation, shot.Tracking); } + // Where a ballistic round is at a sim time: the one statement of the round's line, from its frozen origin along its + // frozen direction at its frozen speed, held at the FireRange point once it has flown that far. Pure: it reads the + // shot alone, and the simulation's own arithmetic (Step, Commit, Apply) does not call it. Presenters draw rounds here. + public static float2 RoundAt(in PendingShot shot, float time) + { + if (shot.Speed <= .01f) return shot.FireOrigin.xz; + var flown = clamp(time - shot.FireTime, 0f, shot.FireRange / shot.Speed); + return shot.FireOrigin.xz + shot.TravelDirection * (shot.Speed * flown); + } + // How far a target can and does move off the line of fire in the solution window: the term DeviationProbability // and HitProbability both add. Behaviour (its observed unpredictability across the line of sight) realises the // evasion and capability (its envelope's reach) caps it, so a coasting ship evades nothing whatever it could do @@ -755,6 +765,7 @@ public static class FireControl Spread = weapon.Spread, FireRange = fireRange, FireTime = now, + Speed = weapon.Velocity, FireOrigin = origin, FireTargetPosition = targetPosition, FireTargetVelocity = targetVelocity, @@ -1999,6 +2010,8 @@ public struct PendingShot public float3 FireTargetPosition; public float3 FireTargetVelocity; public float FireTime; + // The weapon's Velocity at Fire: frozen with the rest, so RoundAt states the same line whatever happens to the stat later. + public float Speed; public float CommitTime; public float ArrivalTime; diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs new file mode 100644 index 000000000..cd3582d70 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -0,0 +1,127 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using System.Linq; +using Xunit; +using static CultMath.math; +using float2 = CultMath.float2; +using float3 = CultMath.float3; + +// Ballistic flight: FireControl.RoundAt is the one statement of where a fired round is at a sim time, from the +// PendingShot's frozen origin, direction, speed and fire time. Unity draws rounds there and integrates nothing, so +// these tests pin the line itself against first principles (the shooter's position, the aim, the authored velocity) +// rather than against the code that computes it. Shares the Cut 12.4 engagement fixture. +public sealed partial class FireControlCut124Tests +{ + private const float BallisticSpeed = 20f; + + // A no-lock proximity round: flies the aim line (2,1) to max range, bursting there. + private Engagement FireBurstRound(out float3 origin) + { + var e = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, fuse: WeaponFuse.Proximity, blastRadius: 4f, + damage: 100f, weaponRange: NoLockRange); + Aim(e, float2(2, 1)); + e.Shooter.SetTarget(TargetRef.None); + origin = e.Shooter.Position; + FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + return e; + } + + [Fact] + public void RoundAtIsTheFiredLine() + { + // Burst round: leaves the shooter, is where its own speed puts it a second in, and ends on the burst point. + var e = FireBurstRound(out var origin); + var burst = SafeAssert.OnlyShot(e.Zone); + var aim = normalize(float2(2, 1)); + + var start = FireControl.RoundAt(burst, burst.FireTime); + Assert.Equal(origin.x, start.x, 3); + Assert.Equal(origin.z, start.y, 3); + + var second = FireControl.RoundAt(burst, burst.FireTime + 1f); + Assert.Equal(origin.x + aim.x * BallisticSpeed, second.x, 3); + Assert.Equal(origin.z + aim.y * BallisticSpeed, second.y, 3); + + var arrival = FireControl.RoundAt(burst, burst.ArrivalTime); + Assert.Equal(burst.BurstPosition.x, arrival.x, 3); + Assert.Equal(burst.BurstPosition.z, arrival.y, 3); + + // Past the range it is held at the FireRange point, however late it is asked. + var held = FireControl.RoundAt(burst, burst.FireTime + NoLockRange / BallisticSpeed + 50f); + Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); + Assert.Equal(origin.z + aim.y * NoLockRange, held.y, 3); + // Before it was fired it is still at the origin, not behind it. + var before = FireControl.RoundAt(burst, burst.FireTime - 5f); + Assert.Equal(origin.x, before.x, 3); + Assert.Equal(origin.z, before.y, 3); + + // Direct round at a stationary target 100 ahead (+z): arrives on the target, and holds at the range point. + var d = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, weaponRange: 1000f); + var dOrigin = d.Shooter.Position; + FireControl.Fire(d.Weapon, d.WeaponItem, d.Shooter); + var direct = SafeAssert.OnlyShot(d.Zone); + var hit = FireControl.RoundAt(direct, direct.ArrivalTime); + Assert.Equal(d.Target.Position.x, hit.x, 2); + Assert.Equal(d.Target.Position.z, hit.y, 2); + Assert.Equal(dOrigin.z + 100f / 2f, FireControl.RoundAt(direct, direct.FireTime + 2.5f).y, 2); + var heldDirect = FireControl.RoundAt(direct, direct.FireTime + direct.FireRange / BallisticSpeed + 99f); + Assert.Equal(dOrigin.z + direct.FireRange, heldDirect.y, 2); + } + + // A round with no speed (the weapon authors Velocity 0, or effectively 0) does not fly: it is at its origin at any + // time, and is not stretched along its line by a vanishing speed over the range it would need forever to cover. + [Fact] + public void ARoundWithNoSpeedStaysAtItsOrigin() + { + var e = Build(TestSettings(), SolidShape(5, 4), velocity: .005f, weaponRange: 1000f); + var origin = e.Shooter.Position; + FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + var shot = SafeAssert.OnlyShot(e.Zone); + + var late = FireControl.RoundAt(shot, shot.FireTime + 5000f); + Assert.Equal(origin.x, late.x, 3); + Assert.Equal(origin.z, late.y, 3); + } + + [Fact] + public void SpeedIsFrozenAtFire() + { + var e = FireBurstRound(out var origin); + var shot = SafeAssert.OnlyShot(e.Zone); + Assert.Equal(BallisticSpeed, shot.Speed, 4); + + // The stat changes after the round left, and the weapon re-reads it. + ((InstantWeaponData) e.Weapon.WeaponData).Velocity = Constant(BallisticSpeed * 5f); + e.Weapon.Execute(0f); + Assert.Equal(BallisticSpeed * 5f, e.Weapon.Velocity, 4); + + var after = SafeAssert.OnlyShot(e.Zone); + Assert.Equal(BallisticSpeed, after.Speed, 4); + var aim = normalize(float2(2, 1)); + var at = FireControl.RoundAt(after, after.FireTime + 1f); + Assert.Equal(origin.x + aim.x * BallisticSpeed, at.x, 3); + Assert.Equal(origin.z + aim.y * BallisticSpeed, at.y, 3); + } + + [Fact] + public void RoundAtIsPure() + { + var e = FireBurstRound(out _); + var shot = SafeAssert.OnlyShot(e.Zone); + var time = e.Zone.Time; + var entities = e.Zone.Entities.ToList(); + var positions = entities.Select(x => x.Position).ToList(); + var velocities = entities.Select(x => x.Velocity).ToList(); + + for (var i = 0; i < 25; i++) + FireControl.RoundAt(shot, shot.FireTime + i * .37f); + + Assert.Equal(time, e.Zone.Time); + Assert.True(e.Zone.PendingShots.Count == 1 && e.Zone.PendingShots[0].Equals(shot), "the pending shot changed"); + Assert.True(e.Zone.Entities.SequenceEqual(entities), "the zone's entities changed"); + Assert.True(entities.Select(x => x.Position).SequenceEqual(positions), "an entity moved"); + Assert.True(entities.Select(x => x.Velocity).SequenceEqual(velocities), "an entity's velocity changed"); + } +} From aaab561f988090e10f72784838d9f0722dee53e2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:19:22 +0200 Subject: [PATCH 05/49] Consumable host: initialise at activation, dispose at expiry, instantiate consumables ActivateConsumable runs Initialize on the effect's behaviours; expiry disposes them before Resolver.Forget, so a consumable's modifiers exist exactly while its effect is active. ItemManager.CreateInstance returns a ConsumableItem for a consumable design. StatResolver.DetachModifier drops an entry left with nothing attached, so ModifierEntryCount returns to baseline. LoadoutTests' hand-built ConsumableItem goes through CreateInstance. Builds. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Entity.cs | 13 ++++++++++++- Assets/Scripts/ServerShared/ItemManager.cs | 3 +++ Assets/Scripts/ServerShared/StatResolver.cs | 3 +++ tests/Aetheria.Shared.Tests/ConsumableTests.cs | 2 +- tests/Aetheria.Shared.Tests/LoadoutTests.cs | 6 +----- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 1da6e68d8..9c4929058 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -860,7 +860,13 @@ public abstract class Entity public void ActivateConsumable(ConsumableItem item) { - _activeConsumables.Add(new ConsumableItemEffect(item, this)); + var effect = new ConsumableItemEffect(item, this); + // The same hook Activate runs over Equipment: a behaviour that computes its targets there (StatModifier) + // would otherwise reach its second tick uninitialised. + foreach (var behavior in effect.Behaviors) + if (behavior is IInitializableBehavior initializable) + initializable.Initialize(); + _activeConsumables.Add(effect); } public ConsumableItemEffect FindActiveConsumable(ConsumableItemData data) @@ -1453,6 +1459,11 @@ public abstract class Entity // Cut 2 Gate 1 fix (docs/stats-and-power-cut.md): an expired consumable dropped out of this // list without ever telling the resolver, leaving its generation/cache/modifier entries // reachable (keyed by this ConsumableItemEffect instance) for the rest of the process. + // A StatModifier's attachments are keyed by its target items, not by the effect, so Forget alone + // leaves a boost attached to its targets: dispose each behaviour first. + foreach (var behavior in _activeConsumables[i].Behaviors) + if (behavior is IDisposable disposable) + disposable.Dispose(); Resolver.Forget(_activeConsumables[i]); _activeConsumables.RemoveAt(i--); } diff --git a/Assets/Scripts/ServerShared/ItemManager.cs b/Assets/Scripts/ServerShared/ItemManager.cs index d9c42d02a..884179e4e 100644 --- a/Assets/Scripts/ServerShared/ItemManager.cs +++ b/Assets/Scripts/ServerShared/ItemManager.cs @@ -183,6 +183,9 @@ public class ItemManager }; } + if (design is ConsumableItemData) + return new ConsumableItem { Data = l.Design, Lot = lot }; + return new CompoundCommodity { Data = l.Design, Lot = lot diff --git a/Assets/Scripts/ServerShared/StatResolver.cs b/Assets/Scripts/ServerShared/StatResolver.cs index bbd3ea542..7062fbd9e 100644 --- a/Assets/Scripts/ServerShared/StatResolver.cs +++ b/Assets/Scripts/ServerShared/StatResolver.cs @@ -141,6 +141,9 @@ public class StatResolver { set.Scale.Remove(modifierKey); set.Constant.Remove(modifierKey); + // An entry with nothing attached is no entry: a modifier that is gone leaves no trace in the resolver. + if (set.Scale.Count == 0 && set.Constant.Count == 0) + _modifiers.Remove(entry); } _cache.Remove(entry); } diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs index 3fdd6aad3..885e4e111 100644 --- a/tests/Aetheria.Shared.Tests/ConsumableTests.cs +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -113,7 +113,7 @@ public sealed class ConsumableTests : IDisposable Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); Assert.True(resolver.ModifierEntryCount > baseline); - Tick(rig, 3, .3f); // 0.9s elapsed in all: expired on the third of these ticks + Tick(rig, 3, .3f); // 1.5s elapsed in all: the effect ran out on the fourth tick Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); Assert.Equal(baseline, resolver.ModifierEntryCount); } diff --git a/tests/Aetheria.Shared.Tests/LoadoutTests.cs b/tests/Aetheria.Shared.Tests/LoadoutTests.cs index 1f2c9c842..303ca8f06 100644 --- a/tests/Aetheria.Shared.Tests/LoadoutTests.cs +++ b/tests/Aetheria.Shared.Tests/LoadoutTests.cs @@ -760,11 +760,7 @@ public sealed class LoadoutTests : IDisposable var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { }); var hullItem = (EquippableItem) items.CreateInstance(items.CreateLot(hullData, maker, .5f)); var ship = new Ship(items, null, hullItem, new EntitySettings()); - // ItemManager.CreateInstance has no ConsumableItemData branch (it only special-cases EquippableItemData, - // falling back to CompoundCommodity otherwise); build the instance directly, as that method does for - // EquippableItem. - var consumableLot = items.CreateLot(consumableData, maker, .5f); - var consumableItem = new ConsumableItem { Data = cache.RefOf(consumableData), Lot = consumableLot }; + var consumableItem = (ConsumableItem) items.CreateInstance(items.CreateLot(consumableData, maker, .5f)); var effect = new ConsumableItemEffect(consumableItem, ship); effect.Update(5f); // half the duration elapsed -> progress .5 -> effectiveness .5 on a linear ramp From aa9288d9deb564c58ea3fb239f21e30859a2f2af Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:19:23 +0200 Subject: [PATCH 06/49] Projectile is drawn on FireControl.RoundAt and integrates nothing Deletes the Time.deltaTime gravity/drag/position integration and the range-distance kill from Projectile and the UnityEngine.Random jitter, barrel direction and inherited velocity from ProjectileManager. A round holds a copy of its PendingShot and draws at RoundAt(Zone.Time + Clock.Lead) with a barrel offset decaying over BlendTime; ShotResolved ends a hit or burst at the outcome's BurstPoint, a miss flies to the FireRange clamp. Unity compile not yet run at this commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Gameplay/Weapons/Projectile.cs | 100 +++++++++++------- .../Gameplay/Weapons/ProjectileManager.cs | 27 ++--- 2 files changed, 70 insertions(+), 57 deletions(-) diff --git a/Assets/Scripts/Gameplay/Weapons/Projectile.cs b/Assets/Scripts/Gameplay/Weapons/Projectile.cs index 70b591649..4e38ad549 100644 --- a/Assets/Scripts/Gameplay/Weapons/Projectile.cs +++ b/Assets/Scripts/Gameplay/Weapons/Projectile.cs @@ -1,59 +1,87 @@ -using System.Collections; +using System; +using System.Collections; +using UniRx; using UnityEngine; using static CultMath.math; -// Cut 3 (docs/fire-control-cut.md): the raycast, shield branch and SendHit are deleted -- FireControl already -// decided this shot's fate before this object was ever spawned (R8). What is left is pure flight-path -// presentation: fly, and disappear at Range or ShotId's resolution, whichever comes first. -// Cut 6b, 6.2 (12.4(b): Apply's own switch on the frozen Fuse decides this now, not Step): the flak-round -// blast resolves in the simulation, through FireControl.Detonate -- a MonoBehaviour calling it would be -// exactly the authority Cut 3 spent itself deleting, so the blast-distance/blast-radius fields this object -// used to carry for that are gone. This object's job stays fly-and-disappear. +// A ballistic round, drawn on FireControl.RoundAt and deciding nothing. The simulation owns the round's whole flight +// (origin, direction, speed, fire time, arrival, range: the PendingShot copy taken at spawn), so this object stores no +// velocity or position of its own and integrates nothing: each frame it asks RoundAt where the round is at sim time +// plus the clock's Lead (the same draw-ahead ships use), and the simulation's ShotResolved ends a round that hit or +// burst. A round that missed flies on to the FireRange point RoundAt clamps at, and is done there. The barrel is only +// where the round is first seen: its offset from the sim line decays to zero over BlendTime sim seconds. The trail's +// fade after the end stays real time (pure presentation, ruling sim-speed-presentation). public class Projectile : MonoBehaviour { public TrailRenderer Trail; - public float Gravity; - public float Drag = .1f; public Prototype HitEffect; + // Sim seconds over which the barrel's offset from the sim line decays to nothing. + public float BlendTime = .1f; + + private PendingShot _shot; + private Zone _zone; + private Vector3 _barrelOffset; private bool _alive; + private IDisposable _binding; - // Cut 3: FireControl.Fire's ShotId -- this projectile's handle onto Zone.ShotCommitted/ShotResolved. - public int ShotId { get; set; } - public Zone Zone { get; set; } - - public Vector3 StartPosition { get; set; } - public Vector3 Velocity { get; set; } - public Entity SourceEntity { get; set; } - public float Range { get; set; } - - private void OnEnable() + // Takes the frozen shot, the zone whose sim time it is drawn at, and where the barrel is as this round is first seen. + public void Launch(Zone zone, PendingShot shot, Vector3 barrel) { + _zone = zone; + _shot = shot; + var origin = shot.FireOrigin; + _barrelOffset = barrel - new Vector3(origin.x, origin.y, origin.z); + _binding?.Dispose(); + _binding = zone.ShotResolved.Where(o => o.ShotId == shot.ShotId).Subscribe(Resolve); _alive = true; + transform.forward = new Vector3(shot.TravelDirection.x, 0, shot.TravelDirection.y); + transform.position = barrel; + } + + private Vector3 At(CultMath.float2 planar, float time) + { + var blend = BlendTime > 0 ? saturate(1 - (time - _shot.FireTime) / BlendTime) : 0f; + return new Vector3(planar.x, _shot.FireOrigin.y, planar.y) + _barrelOffset * blend; } - // Update is called once per frame void Update() { - if (SourceEntity == null) return; - - if(_alive) - { - var t = transform; - Velocity -= Vector3.up * (Gravity * Time.deltaTime); - Velocity *= max(0, 1 - Drag * Time.deltaTime); - t.forward = Velocity.normalized; - - transform.position += Velocity * Time.deltaTime; - var distanceTraveled = (transform.position - StartPosition).magnitude; - if(distanceTraveled > Range) - StartCoroutine(Kill()); - } + if (!_alive) return; + var time = _zone.Time + ActionGameManager.Clock.Lead; + transform.position = At(FireControl.RoundAt(_shot, time), time); + var end = _shot.Speed > .01f ? _shot.FireTime + _shot.FireRange / _shot.Speed : _shot.FireTime; + if (time >= end) Finish(); } - IEnumerator Kill() + // The simulation's verdict. A round that hit or burst stops where the sim put it, at the outcome's burst point, + // else where it arrives on its line; a miss keeps flying. + private void Resolve(ShotOutcome outcome) + { + if (!_alive || (outcome.Result != ShotResult.Hit && outcome.Result != ShotResult.Burst)) return; + var planar = outcome.HasBurstPoint ? outcome.BurstPoint : FireControl.RoundAt(_shot, _shot.ArrivalTime); + transform.position = At(planar, _shot.ArrivalTime); + if (HitEffect != null) HitEffect.Instantiate().position = transform.position; + Finish(); + } + + private void Finish() { _alive = false; + _binding?.Dispose(); + _binding = null; + StartCoroutine(Fade()); + } + + void OnDisable() + { + _alive = false; + _binding?.Dispose(); + _binding = null; + } + + IEnumerator Fade() + { var startTime = Time.time; var lifetime = Trail.time; while (Time.time - startTime < lifetime) diff --git a/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs b/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs index 56cfdcf80..83eba9ea4 100644 --- a/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs +++ b/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs @@ -1,32 +1,17 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using Random = UnityEngine.Random; +using UnityEngine; public class ProjectileManager : InstantWeaponEffectManager { public Prototype ProjectilePrototype; - public bool InheritVelocity; + // The round is drawn on the line the simulation flies it (FireControl.RoundAt), so there is nothing to aim, jitter + // or inherit here: spread is priced by FireControl, and the sim's round inherits nothing from its shooter. public override void Fire(InstantWeapon weapon, EquippedItem item, EntityInstance source, EntityInstance target, int shotId) { + var zone = source.Entity.Zone; + if (!zone.TryGetShot(shotId, out var shot)) return; var p = ProjectilePrototype.Instantiate(); - var hp = item.Hardpoint; - var barrel = source.GetBarrel(hp); - var angle = weapon.Spread / 2; - p.ShotId = shotId; - p.SourceEntity = source.Entity; - p.Velocity = Quaternion.Euler( - Random.Range(-angle, angle), - Random.Range(-angle, angle), - Random.Range(-angle, angle)) * - barrel.forward * - weapon.Velocity; - p.StartPosition = p.transform.position = barrel.position + p.Velocity * (Random.value * Time.deltaTime); - if(InheritVelocity) - p.Velocity += new Vector3(source.Entity.Velocity.x, 0, source.Entity.Velocity.y); - p.Range = weapon.Range; - p.Zone = source.Entity.Zone; + p.Launch(zone, shot, source.GetBarrel(item.Hardpoint).position); p.Trail.Clear(); } } From 6dcf33d11cefd4b65c0e2f958eedf54ff103cd58 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:22:00 +0200 Subject: [PATCH 07/49] Vapour clouds: Zone owns them, a VapourDump behaviour vents one (union 42) Zone.Vent is the one adder, StepClouds the one remover, Zone.Obscuration the one reader: the product of (1 - opacity) over clouds whose disc the segment touches (closest point of the segment to the centre within Radius), 1f when none. Derived from live clouds at each call, never cached. VapourDump vents once on its first run, drifting with its venter. Nothing reads Obscuration yet (Sensor.Gain takes it in the next commit); this commit builds. Co-Authored-By: Claude Opus 5.5 --- .../ServerShared/Behaviors/Behaviors.cs | 1 + .../ServerShared/Behaviors/VapourDump.cs | 61 ++++ .../ServerShared/Behaviors/VapourDump.cs.meta | 11 + Assets/Scripts/ServerShared/VapourClouds.cs | 31 ++ .../Scripts/ServerShared/VapourClouds.cs.meta | 11 + Assets/Scripts/ServerShared/Zone.cs | 38 +++ .../Aetheria.Shared.Tests/VapourCloudTests.cs | 273 ++++++++++++++++++ 7 files changed, 426 insertions(+) create mode 100644 Assets/Scripts/ServerShared/Behaviors/VapourDump.cs create mode 100644 Assets/Scripts/ServerShared/Behaviors/VapourDump.cs.meta create mode 100644 Assets/Scripts/ServerShared/VapourClouds.cs create mode 100644 Assets/Scripts/ServerShared/VapourClouds.cs.meta create mode 100644 tests/Aetheria.Shared.Tests/VapourCloudTests.cs diff --git a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs index 9f2e292a6..7515d55e7 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs @@ -187,6 +187,7 @@ public interface IPowerConsumer // superseded draft quoted -- checked directly here rather than trusted from that document. Union(39, typeof(TargetingSystemData)), Union(40, typeof(MineLayerData)), + Union(42, typeof(VapourDumpData)), JsonObject(MemberSerialization.OptIn)] public abstract class BehaviorData { diff --git a/Assets/Scripts/ServerShared/Behaviors/VapourDump.cs b/Assets/Scripts/ServerShared/Behaviors/VapourDump.cs new file mode 100644 index 000000000..3e035f9b8 --- /dev/null +++ b/Assets/Scripts/ServerShared/Behaviors/VapourDump.cs @@ -0,0 +1,61 @@ +using MessagePack; +using Newtonsoft.Json; + +// A coolant dump that becomes a vapour cloud: the first time it runs it vents one cloud around its venter, +// drifting with the venter's velocity. Radius, Opacity and Lifetime are evaluated through the host, so role +// quality applies as for any other stat. Later runs vent nothing. +[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable] +public class VapourDumpData : BehaviorData +{ + [Inspectable, JsonProperty("radius"), Key(1), RuntimeInspectable] + public PerformanceStat Radius = new PerformanceStat(); + + // How much of a sight line's signal a fresh cloud removes, in [0, 1]. + [Inspectable, JsonProperty("opacity"), Key(2), RuntimeInspectable] + public PerformanceStat Opacity = new PerformanceStat(); + + // Seconds from venting until the cloud has faded away and is removed. + [Inspectable, JsonProperty("lifetime"), Key(3), RuntimeInspectable] + public PerformanceStat Lifetime = new PerformanceStat(); + + public override Behavior CreateInstance(EquippedItem item) + { + return new VapourDump(this, item); + } + + public override Behavior CreateInstance(ConsumableItemEffect item) + { + return new VapourDump(this, item); + } +} + +public class VapourDump : Behavior +{ + private VapourDumpData _data; + private bool _vented; + + public VapourDump(VapourDumpData data, EquippedItem item) : base(data, item) + { + _data = data; + } + + public VapourDump(VapourDumpData data, ConsumableItemEffect item) : base(data, item) + { + _data = data; + } + + public override bool Execute(float dt) + { + if (_vented) return true; + _vented = true; + Entity.Zone.Vent(new VapourCloud + { + Body = new KinematicBody { Position = Entity.Position.xz, Velocity = Entity.Velocity }, + Radius = Evaluate(_data.Radius), + Opacity = Evaluate(_data.Opacity), + Lifetime = Evaluate(_data.Lifetime), + Venter = Entity + }); + return true; + } +} diff --git a/Assets/Scripts/ServerShared/Behaviors/VapourDump.cs.meta b/Assets/Scripts/ServerShared/Behaviors/VapourDump.cs.meta new file mode 100644 index 000000000..86ef08913 --- /dev/null +++ b/Assets/Scripts/ServerShared/Behaviors/VapourDump.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 961a74683c574807a300eb26694f206d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/VapourClouds.cs b/Assets/Scripts/ServerShared/VapourClouds.cs new file mode 100644 index 000000000..d6d1aaa24 --- /dev/null +++ b/Assets/Scripts/ServerShared/VapourClouds.cs @@ -0,0 +1,31 @@ +using CultMath; +using static CultMath.math; +using float2 = CultMath.float2; + +// A vented vapour cloud: a free-floating disc that thins every sensor sight line through it. Not an Entity, +// never priced or rolled, never saved. Zone.Vent is the only adder and Zone's cloud step the only remover; +// Zone.Obscuration is the one reader detection uses. +public sealed class VapourCloud +{ + public FloatingBodyId Id; + public KinematicBody Body; + public double VentedAt; + public float Lifetime; + public float Radius; + // The opacity at the moment of venting; it fades linearly to nothing over Lifetime. + public float Opacity; + public Entity Venter; + + public float OpacityAt(double now) => + Lifetime <= 0f ? 0f : saturate(Opacity) * saturate(1f - (float) (now - VentedAt) / Lifetime); + + // Whether the segment a-b reaches the disc: the closest point of the segment to the centre lies within the + // radius. A cloud past either end of the segment touches it only if it is near that end. + public bool Touches(float2 a, float2 b) + { + var ab = b - a; + var length2 = lengthsq(ab); + var t = length2 > 0f ? saturate(dot(Body.Position - a, ab) / length2) : 0f; + return lengthsq(Body.Position - (a + ab * t)) <= Radius * Radius; + } +} diff --git a/Assets/Scripts/ServerShared/VapourClouds.cs.meta b/Assets/Scripts/ServerShared/VapourClouds.cs.meta new file mode 100644 index 000000000..cdaa4b80d --- /dev/null +++ b/Assets/Scripts/ServerShared/VapourClouds.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ced45a9738974452b0065b57ee527e8a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Zone.cs b/Assets/Scripts/ServerShared/Zone.cs index df07d1f57..0aa822775 100644 --- a/Assets/Scripts/ServerShared/Zone.cs +++ b/Assets/Scripts/ServerShared/Zone.cs @@ -20,6 +20,10 @@ public class Zone // Laid mines, in lay order. Zone owns their existence: Lay is the one adder, the mine step the one remover, // and every removal is a blast. Runtime state, never saved. public ReactiveCollection Mines = new ReactiveCollection(); + + // Vented vapour clouds, in vent order. Zone owns their existence: Vent is the one adder, StepClouds the one + // remover, and Obscuration the one reader that detection uses. Runtime state, never saved. + public ReactiveCollection Clouds = new ReactiveCollection(); public Dictionary Planets = new Dictionary(); public Dictionary PlanetInstances = new Dictionary(); @@ -225,6 +229,7 @@ public class Zone foreach (var entity in Entities.ToArray()) entity.Update(deltaTime); StepMines(deltaTime); + StepClouds(deltaTime); // Cut 3: after every entity has had its chance to fire this tick, age and resolve the shots that // firing queued. A shot fired this tick with a flight time shorter than CommitHorizon commits and @@ -238,6 +243,39 @@ public class Zone Mines.Add(mine); } + public void Vent(VapourCloud cloud) + { + cloud.Id = NextBodyId(); + cloud.VentedAt = _time; + Clouds.Add(cloud); + } + + // The fraction of a sight line's signal that survives the clouds between `a` and `b`: the product of + // (1 - opacity now) over every cloud whose disc the segment touches, so 1f when none does. Derived from the + // live clouds at each call and stored nowhere; Sensor.Gain is where it enters detection. + public float Obscuration(float2 a, float2 b) + { + var through = 1f; + for (var i = 0; i < Clouds.Count; i++) + { + var cloud = Clouds[i]; + if (cloud.Touches(a, b)) through *= 1f - cloud.OpacityAt(_time); + } + return through; + } + + // After StepMines: each cloud drifts as a KinematicBody on the zone's force, as a mine does, and is removed + // once its lifetime has run out. Nothing else removes a cloud. + private void StepClouds(float deltaTime) + { + var settings = _itemManager.GameplaySettings; + foreach (var cloud in Clouds.ToArray()) + { + cloud.Body.Step(deltaTime, GetForce(cloud.Body.Position), settings); + if (cloud.VentedAt + cloud.Lifetime <= _time) Clouds.Remove(cloud); + } + } + // After the entity loop and before FireControl.Step, in lay order: each mine drifts as a KinematicBody, is // triggered by the first eligible hull its armed disc touches, and detonates through FireControl.Detonate at // its trigger plus fuse delay or at its lifetime, whatever its layer does (ruling mine-owner-leash). The diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs new file mode 100644 index 000000000..d829de565 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using CultMath; +using GameCult.Caching; +using UniRx; +using Xunit; +using static CultMath.math; +using float2 = CultMath.float2; +using float3 = CultMath.float3; +using int2 = CultMath.int2; + +// Vapour clouds in the sim (aetheria-release cut vapour-cloud, ruling vapour-cloud-obscuration): a VapourDump +// behaviour vents a zone cloud that thins every sensor sight line through it, both ways, through Sensor.Gain's +// one detection rule; a lock falls by the existing detection-threshold rule and nothing else. The scene is two +// ships facing each other along z with a fixed visibility, so what a sensor gathers is a number the clouds +// alone change: steady info is visibility x sensitivity / (distance x TargetInfoDecay), and visibility 100, +// sensitivity 1, decay .5 puts it at .5 at range 400 (above the .1 threshold) and saturated at range 40. +public sealed class VapourCloudTests : IDisposable +{ + private const float Dt = .05f; + private const float Threshold = .1f; + private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-vapour-" + Guid.NewGuid().ToString("N")); + private readonly List _caches = new List(); + private static readonly object Glow = new object(); + + public VapourCloudTests() => Directory.CreateDirectory(_root); + + public void Dispose() + { + foreach (var c in _caches) c.Dispose(); + Directory.Delete(_root, true); + } + + private static PerformanceStat Constant(float v) => new PerformanceStat { Min = v, Max = v }; + + private static Shape Solid(int w, int h) + { + var shape = new Shape(w, h); + foreach (var cell in shape.AllCoordinates) shape[cell] = true; + return shape; + } + + private sealed class Lab + { + public ItemManager Items; + public Zone Zone; + public Ship Observer; + public Ship Target; + public EquippedItem Dump; + public LockWeapon Lock; + public int Lot = 1; + public bool HoldTarget = true; + } + + private Lab Build(float distance, float radius = 20f, float opacity = .9f, float lifetime = 1000f) + { + var settings = new GameplaySettings + { + DefaultEntitySettings = new EntitySettings(), + Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } }, + QualityPriceModifier = new ExponentialLerp(), + TargetDetectionInfoThreshold = Threshold, TargetArmorInfoThreshold = .2f, TargetGearInfoThreshold = .8f, + TargetInfoDecay = .5f, VisibilityDecay = 0f, + FiringArc = 170, CommitHorizon = .5f, SchematicCellSize = 1f + }; + var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true); + _caches.Add(cache); + cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); + cache.Upsert(new HullData + { + Name = "Hull", HullType = HullType.Ship, Shape = Solid(5, 5), Durability = 1000000, Mass = 1000, + Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = new Shape() } } + }); + GearData Gear(string name, HardpointType hardpoint, BehaviorData behavior) => new GearData + { + Name = name, Hardpoint = hardpoint, Shape = new Shape(), Durability = 1, + MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000, + Behaviors = { behavior } + }; + cache.Upsert(Gear("Eye", HardpointType.Tool, new SensorData + { + Sensitivity = Constant(1f), + SensitivityCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }, + PingBoost = Constant(.02f), PingEnergy = Constant(0f), PingVisibility = Constant(5f), + PingRange = Constant(300f), PingCooldown = Constant(4f) + })); + cache.Upsert(Gear("Vent", HardpointType.Tool, new VapourDumpData + { + Radius = Constant(radius), Opacity = Constant(opacity), Lifetime = Constant(lifetime) + })); + cache.Upsert(Gear("LockGun", HardpointType.Sensors, new LockWeaponData + { + Damage = Constant(0), Range = Constant(1000), MinRange = Constant(0), Velocity = Constant(0), + Spread = Constant(0), DamageSpread = Constant(0), Penetration = Constant(0), Count = Constant(1), + BurstTime = Constant(0), Cooldown = Constant(1000), + DamageCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }, + LockSpeed = Constant(1), SensorImpact = Constant(1), LockAngle = Constant(170), + DirectionImpact = Constant(1), Decay = Constant(1) + })); + cache.FlushAsync().Wait(); + + var ledger = new ProvenanceLedger(); + for (var i = 1; i <= 50; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 }; + var items = new ItemManager(cache, ledger, settings, _ => { }); + var zone = new Zone(items, new PlanetSettings(), new ZonePack { Radius = 5000 }, new GalaxyZone { Name = "Vapour", Owner = null }, null); + var lab = new Lab { Items = items, Zone = zone }; + + EquippableItem Make(string name) => new EquippableItem + { + Data = items.ItemData.RefOf(items.ItemData.GetByName(name)), Durability = 1, Lot = lab.Lot++ + }; + Ship NewShip() + { + var hull = items.ItemData.RefOf(items.ItemData.GetByName("Hull")); + var ship = new Ship(items, zone, new EquippableItem { Data = hull, Durability = 1000000, Lot = lab.Lot++ }, new EntitySettings()); + Assert.True(ship.TryEquip(Make("Eye"))); + return ship; + } + + lab.Observer = NewShip(); + Assert.True(lab.Observer.TryEquip(Make("LockGun"), new int2(0, 0))); + lab.Target = NewShip(); + var dump = Make("Vent"); + Assert.True(lab.Target.TryEquip(dump)); + lab.Dump = lab.Target.Equipment.Single(x => x.EquippableItem == dump); + // Held off until a test pulls the trigger: the dump vents on its first run. + lab.Dump.Enabled.Value = false; + + zone.Entities.Add(lab.Observer); + zone.Entities.Add(lab.Target); + lab.Observer.Activate(); + lab.Target.Activate(); + lab.Observer.Position = float3(0, 0, 0); + lab.Observer.Aim = float3(0, 0, 1); + lab.Target.Position = float3(0, 0, distance); + lab.Target.Aim = float3(0, 0, -1); + lab.Observer.SetIff(lab.Target, true); + lab.Target.SetIff(lab.Observer, true); + zone.Update(0f); + lab.Lock = lab.Observer.Weapons.OfType().Single(); + return lab; + } + + // One tick: both ships stay where they were put, facing each other, each a fixed visibility to the other's sensor. + private static void Tick(Lab lab) + { + foreach (var ship in new[] { lab.Observer, lab.Target }) + { + ship.VisibilitySources[Glow] = 100f; + if (ship != lab.Target || lab.HoldTarget) ship.Velocity = float2.zero; + } + lab.Observer.Direction = float2(0, 1); + lab.Target.Direction = float2(0, -1); + lab.Zone.Update(Dt); + } + + private static void Run(Lab lab, int ticks) + { + for (var i = 0; i < ticks; i++) Tick(lab); + } + + private static float Info(Ship watcher, Ship watched) => watcher.EntityInfoGathered[watched]; + + // The observer has settled on the target and holds a full lock on it: the state a cloud has to break. + private static void Establish(Lab lab) + { + Run(lab, 100); + Assert.True(Info(lab.Observer, lab.Target) > Threshold, "the observer never gathered the target"); + Assert.Contains(lab.Target, lab.Observer.VisibleEnemies); + lab.Observer.SetTarget(lab.Target); + Run(lab, 100); + Assert.True(lab.Lock.IsLocked, "the observer never locked the target"); + Assert.Empty(lab.Zone.Clouds); + } + + private static void Vent(Lab lab) + { + lab.Dump.Enabled.Value = true; + for (var i = 0; i < 5 && lab.Zone.Clouds.Count == 0; i++) Tick(lab); + Assert.Single(lab.Zone.Clouds); + } + + private static VapourCloud Hand(Zone zone, float2 at, float radius, float opacity, float lifetime = 1000f) + { + var cloud = new VapourCloud { Body = new KinematicBody { Position = at }, Radius = radius, Opacity = opacity, Lifetime = lifetime }; + zone.Vent(cloud); + return cloud; + } + + [Fact] + public void ACloudFadesAndIsRemoved() + { + var lab = Build(400f); + var cloud = Hand(lab.Zone, float2(0, 0), 10f, .8f, lifetime: 10f); + var a = float2(-5, 0); + var b = float2(5, 0); + Assert.Equal(.2f, lab.Zone.Obscuration(a, b), 5); + + for (var i = 0; i < 5; i++) lab.Zone.Update(1f); + Assert.Equal(.4f, cloud.OpacityAt(lab.Zone.Time), 5); + Assert.Equal(.6f, lab.Zone.Obscuration(a, b), 5); + Assert.Single(lab.Zone.Clouds); + + for (var i = 0; i < 5; i++) lab.Zone.Update(1f); + Assert.Empty(lab.Zone.Clouds); + Assert.Equal(1f, lab.Zone.Obscuration(a, b)); + } + + [Fact] + public void ACloudWithNoLifetimeObscuresNothing() + { + var lab = Build(400f); + var cloud = Hand(lab.Zone, float2(0, 0), 10f, .8f, lifetime: 0f); + Assert.Equal(0f, cloud.OpacityAt(lab.Zone.Time)); + Assert.Equal(1f, lab.Zone.Obscuration(float2(-5, 0), float2(5, 0))); + } + + [Fact] + public void AnEffectVentsOnce() + { + var lab = Build(400f); + lab.HoldTarget = false; + Run(lab, 2); + lab.Target.Velocity = float2(3, 0); + VapourCloud vented = null; + float2 venterVelocity = float2.zero; + float2 venterAt = float2.zero; + using var sub = lab.Zone.Clouds.ObserveAdd().Subscribe(add => + { + vented = add.Value; + venterVelocity = lab.Target.Velocity; + venterAt = lab.Target.Position.xz; + }); + + lab.Dump.Enabled.Value = true; + Run(lab, 100); + + Assert.Single(lab.Zone.Clouds); + Assert.Same(vented, lab.Zone.Clouds.Single()); + Assert.True(length(venterVelocity) > 0f, "the venter was not moving"); + Assert.Equal(venterVelocity, vented.Body.Velocity); + Assert.Equal(venterAt, vented.Body.Position); + Assert.Same(lab.Target, vented.Venter); + } + + [Fact] + public void ACloudThinsOnlyTheSightLinesItTouches() + { + var lab = Build(400f); + Hand(lab.Zone, float2(0, 0), 10f, .6f); + float Through(float2 a, float2 b) => lab.Zone.Obscuration(a, b); + + Assert.Equal(.4f, Through(float2(-50, 0), float2(50, 0)), 5); + Assert.Equal(.4f, Through(float2(50, 0), float2(-50, 0)), 5); + // Passing wide of the centre but within the radius, neither end inside the disc. + Assert.Equal(.4f, Through(float2(-50, 9.9f), float2(50, 9.9f)), 5); + Assert.Equal(1f, Through(float2(-50, 10.1f), float2(50, 10.1f))); + // Ending inside the disc, and a point inside it (the observer inside its own cloud). + Assert.Equal(.4f, Through(float2(-50, 0), float2(5, 0)), 5); + Assert.Equal(.4f, Through(float2(3, 0), float2(3, 0)), 5); + Assert.Equal(1f, Through(float2(30, 0), float2(30, 0))); + // A segment that stops short of the disc, or starts past it, touches nothing even on the same line. + Assert.Equal(1f, Through(float2(-50, 0), float2(-20, 0))); + Assert.Equal(1f, Through(float2(20, 0), float2(50, 0))); + + // Two clouds on one sight line multiply. + Hand(lab.Zone, float2(30, 0), 5f, .5f); + Assert.Equal(.2f, Through(float2(-50, 0), float2(50, 0)), 5); + Assert.Equal(.4f, Through(float2(-50, 0), float2(15, 0)), 5); + } +} From 0fb5ac1dfc19fad4e2095aabf693820140394529 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:22:16 +0200 Subject: [PATCH 08/49] Sensor.Gain takes the obscuration of its sight line Gain stays the one detection rule: both branches are scaled by the fraction of the signal that survives the clouds between observer and target (Zone.Obscuration, exactly 1f with none, so the pre-cloud trace is bit-identical). The entity loop passes it for pings and passive ticks and PassiveRate for the chunk position. Entity.cs is untouched: a lock falls by the existing detection-threshold rule when info does. Adds the sensor-level facts to VapourCloudTests. Builds; not yet run (verify job follows). Co-Authored-By: Claude Opus 5.5 --- .../Scripts/ServerShared/Behaviors/Sensor.cs | 20 +++-- .../Aetheria.Shared.Tests/VapourCloudTests.cs | 79 +++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/Assets/Scripts/ServerShared/Behaviors/Sensor.cs b/Assets/Scripts/ServerShared/Behaviors/Sensor.cs index 8eeadbb7d..d1af7645e 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Sensor.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Sensor.cs @@ -160,17 +160,18 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer var diff = entity.Position.xz - Entity.Position.xz; var dist = length(diff); + var obscuration = Entity.Zone.Obscuration(Entity.Position.xz, entity.Position.xz); float previous, next; Entity.EntityInfoGathered.TryGetValue(entity, out previous); if (!_pingedEntities.Contains(entity) && dist < _pingRadius) { _pingedEntities.Add(entity); - next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.Sensitivity), Evaluate(_data.PingBoost), dist, dt, true)); + next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.Sensitivity), Evaluate(_data.PingBoost), dist, dt, true, obscuration)); } else { var angle = acos(dot(forward, normalize(diff))); - next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.Sensitivity), _data.SensitivityCurve.Evaluate(angle / PI), dist, dt, false)); + next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.Sensitivity), _data.SensitivityCurve.Evaluate(angle / PI), dist, dt, false, obscuration)); } next *= 1 - ItemManager.GameplaySettings.TargetInfoDecay * dt; //Context.Log($"{entity.Name} visibility {(int)(previous * 100)}% -> {(int)(next * 100)}%"); @@ -182,12 +183,14 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer // Mining Cut 3 (docs/mining-cut-refresh.md): the one detection gain rule -- the info one observation of a // target adds. A ping adds `visibility × sensitivity × PingBoost × distance` once; a passive tick adds // `visibility × sensitivity × curve(angle/π) × dt / distance`. `response` is PingBoost for a ping and the - // sensitivity curve at the bearing for a passive tick. The entity loop above and the chunk query - // (Entity.ChunkInfo, through PassiveRate) both call this; nothing else multiplies these terms. - public static float Gain(float visibility, float sensitivity, float response, float distance, float dt, bool ping) => - ping + // sensitivity curve at the bearing for a passive tick. `obscuration` is the fraction of the signal that + // survives the vapour clouds on the sight line (Zone.Obscuration, 1f with none), scaling both. The entity + // loop above and the chunk query (Entity.ChunkInfo, through PassiveRate) both call this; nothing else + // multiplies these terms. + public static float Gain(float visibility, float sensitivity, float response, float distance, float dt, bool ping, float obscuration) => + (ping ? visibility * sensitivity * response * distance - : visibility * sensitivity * response * dt / distance; + : visibility * sensitivity * response * dt / distance) * obscuration; // The passive gain per second this sensor draws from a target of `visibility` at the planar `position`: // Gain over one second, with the bearing measured from this sensor's own facing exactly as Execute does. @@ -195,7 +198,8 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer { var diff = position - Entity.Position.xz; var angle = acos(dot(Direction.xz, normalize(diff))); - return Gain(visibility, Evaluate(_data.Sensitivity), _data.SensitivityCurve.Evaluate(angle / PI), length(diff), 1f, false); + return Gain(visibility, Evaluate(_data.Sensitivity), _data.SensitivityCurve.Evaluate(angle / PI), length(diff), 1f, false, + Entity.Zone.Obscuration(Entity.Position.xz, position)); } // An upper bound on sensitivity x response over every bearing, so PassiveRate(v, p) <= v x this / distance diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs index d829de565..af80b0143 100644 --- a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs +++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs @@ -189,6 +189,56 @@ public sealed class VapourCloudTests : IDisposable return cloud; } + [Fact] + public void ACloudBreaksALockAtRange() + { + var lab = Build(400f); + Establish(lab); + + Vent(lab); + var ticks = 0; + while (ticks++ < 600 && !lab.Observer.Target.Value.IsNone) Tick(lab); + + Assert.True(lab.Observer.Target.Value.IsNone, "the cloud never broke the target"); + Assert.True(Info(lab.Observer, lab.Target) < Threshold, $"info {Info(lab.Observer, lab.Target)} is above the threshold"); + Run(lab, 3); + Assert.Equal(0f, lab.Lock.Lock); + Assert.DoesNotContain(lab.Target, lab.Observer.VisibleEnemies); + } + + [Fact] + public void AStrongCloseSensorBurnsThrough() + { + var lab = Build(40f); + Establish(lab); + var before = Info(lab.Observer, lab.Target); + + Vent(lab); + Run(lab, 600); + + var after = Info(lab.Observer, lab.Target); + Assert.True(after < before - .2f, $"the cloud thinned nothing: {before} -> {after}"); + Assert.True(after > Threshold, $"info {after} fell below the threshold"); + Assert.Equal(lab.Target, lab.Observer.Target.Value.Entity); + Assert.True(lab.Lock.IsLocked); + } + + [Fact] + public void TheCloudBlindsItsVenter() + { + var lab = Build(400f); + Run(lab, 100); + Assert.True(Info(lab.Target, lab.Observer) > Threshold, "the venter never gathered the observer"); + Assert.Contains(lab.Observer, lab.Target.VisibleEnemies); + + Vent(lab); + Run(lab, 300); + + Assert.True(Info(lab.Target, lab.Observer) < Threshold); + Assert.DoesNotContain(lab.Observer, lab.Target.VisibleEnemies); + Assert.Equal(Info(lab.Observer, lab.Target), Info(lab.Target, lab.Observer), 5); + } + [Fact] public void ACloudFadesAndIsRemoved() { @@ -270,4 +320,33 @@ public sealed class VapourCloudTests : IDisposable Assert.Equal(.2f, Through(float2(-50, 0), float2(50, 0)), 5); Assert.Equal(.4f, Through(float2(-50, 0), float2(15, 0)), 5); } + + // Where the rule is decided: one tick from nothing, the info gathered is exactly what survives the cloud + // times what the open sight line gathers, in both directions. + [Fact] + public void TheGainIsScaledByWhatSurvivesTheCloud() + { + var clear = Build(100f); + Tick(clear); + var cloudy = Build(100f); + Hand(cloudy.Zone, float2(0, 100), 20f, .6f); + Tick(cloudy); + + Assert.True(Info(clear.Observer, clear.Target) > 0f); + Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / Info(clear.Observer, clear.Target), 3); + Assert.Equal(.4f, Info(cloudy.Target, cloudy.Observer) / Info(clear.Target, clear.Observer), 3); + } + + // The chunk path reads the same rule: the passive rate toward a point is scaled by the same survivor fraction. + [Fact] + public void TheChunkRateIsScaledByTheSameFactor() + { + var lab = Build(100f); + var at = float2(0, 100); + var open = lab.Observer.Sensor.PassiveRate(100f, at); + Assert.True(open > 0f); + + Hand(lab.Zone, at, 20f, .6f); + Assert.Equal(.4f, lab.Observer.Sensor.PassiveRate(100f, at) / open, 5); + } } From 2640faa9a6dd035c5987a41f3faceedc7d416b5b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:24:06 +0200 Subject: [PATCH 09/49] VapourCloudTests: drift, ping, wire shape and authored stats Pins the cloud's drift and fade-from-vent-time, the ping branch of Gain, the dump's authored radius/opacity/lifetime and its union 42 keys 1-3. Test-only; builds. Co-Authored-By: Claude Opus 5.5 --- .../Aetheria.Shared.Tests/VapourCloudTests.cs | 82 +++++++++++++++++-- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs index af80b0143..c2300264e 100644 --- a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs +++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs @@ -4,6 +4,8 @@ using System.IO; using System.Linq; using CultMath; using GameCult.Caching; +using GameCult.Caching.MessagePack; +using MessagePack; using UniRx; using Xunit; using static CultMath.math; @@ -83,7 +85,7 @@ public sealed class VapourCloudTests : IDisposable { Sensitivity = Constant(1f), SensitivityCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }, - PingBoost = Constant(.02f), PingEnergy = Constant(0f), PingVisibility = Constant(5f), + PingBoost = Constant(.00002f), PingEnergy = Constant(0f), PingVisibility = Constant(5f), PingRange = Constant(300f), PingCooldown = Constant(4f) })); cache.Upsert(Gear("Vent", HardpointType.Tool, new VapourDumpData @@ -243,6 +245,8 @@ public sealed class VapourCloudTests : IDisposable public void ACloudFadesAndIsRemoved() { var lab = Build(400f); + // Vented 3s into the zone's life: the fade runs from the vent, not from the zone's start. + for (var i = 0; i < 3; i++) lab.Zone.Update(1f); var cloud = Hand(lab.Zone, float2(0, 0), 10f, .8f, lifetime: 10f); var a = float2(-5, 0); var b = float2(5, 0); @@ -258,6 +262,21 @@ public sealed class VapourCloudTests : IDisposable Assert.Equal(1f, lab.Zone.Obscuration(a, b)); } + [Fact] + public void ACloudDriftsWithItsVelocityUntilItFades() + { + var lab = Build(400f); + var cloud = new VapourCloud { Body = new KinematicBody { Position = float2(0, 0), Velocity = float2(10, 0) }, Radius = 5f, Opacity = .5f, Lifetime = 1000f }; + lab.Zone.Vent(cloud); + Assert.Equal(.5f, lab.Zone.Obscuration(float2(0, -20), float2(0, 20)), 5); + + lab.Zone.Update(1f); + + Assert.Equal(10f, cloud.Body.Position.x, 3); + Assert.Equal(1f, lab.Zone.Obscuration(float2(0, -20), float2(0, 20))); + Assert.Equal(.5f, lab.Zone.Obscuration(float2(10, -20), float2(10, 20)), 2); + } + [Fact] public void ACloudWithNoLifetimeObscuresNothing() { @@ -270,7 +289,7 @@ public sealed class VapourCloudTests : IDisposable [Fact] public void AnEffectVentsOnce() { - var lab = Build(400f); + var lab = Build(400f, radius: 17f, opacity: .7f, lifetime: 123f); lab.HoldTarget = false; Run(lab, 2); lab.Target.Velocity = float2(3, 0); @@ -293,13 +312,17 @@ public sealed class VapourCloudTests : IDisposable Assert.Equal(venterVelocity, vented.Body.Velocity); Assert.Equal(venterAt, vented.Body.Position); Assert.Same(lab.Target, vented.Venter); + // Each stat is the authored one, evaluated through the host. + Assert.Equal(17f, vented.Radius); + Assert.Equal(.7f, vented.Opacity); + Assert.Equal(123f, vented.Lifetime); } [Fact] public void ACloudThinsOnlyTheSightLinesItTouches() { var lab = Build(400f); - Hand(lab.Zone, float2(0, 0), 10f, .6f); + var first = Hand(lab.Zone, float2(0, 0), 10f, .6f); float Through(float2 a, float2 b) => lab.Zone.Obscuration(a, b); Assert.Equal(.4f, Through(float2(-50, 0), float2(50, 0)), 5); @@ -316,7 +339,8 @@ public sealed class VapourCloudTests : IDisposable Assert.Equal(1f, Through(float2(20, 0), float2(50, 0))); // Two clouds on one sight line multiply. - Hand(lab.Zone, float2(30, 0), 5f, .5f); + var second = Hand(lab.Zone, float2(30, 0), 5f, .5f); + Assert.NotEqual(first.Id, second.Id); Assert.Equal(.2f, Through(float2(-50, 0), float2(50, 0)), 5); Assert.Equal(.4f, Through(float2(-50, 0), float2(15, 0)), 5); } @@ -329,7 +353,8 @@ public sealed class VapourCloudTests : IDisposable var clear = Build(100f); Tick(clear); var cloudy = Build(100f); - Hand(cloudy.Zone, float2(0, 100), 20f, .6f); + // Mid-way between them and clear of both: only the segment, not either end, touches it. + Hand(cloudy.Zone, float2(0, 50), 10f, .6f); Tick(cloudy); Assert.True(Info(clear.Observer, clear.Target) > 0f); @@ -337,6 +362,33 @@ public sealed class VapourCloudTests : IDisposable Assert.Equal(.4f, Info(cloudy.Target, cloudy.Observer) / Info(clear.Target, clear.Observer), 3); } + // A ping is gathered through the same rule: with a cloud on the sight line the whole trace, ping included, is + // what survives the cloud times the open trace (info is linear in the gain while nothing saturates). + [Fact] + public void ThePingGainIsScaledToo() + { + var clear = Build(100f); + var cloudy = Build(100f); + Hand(cloudy.Zone, float2(0, 50), 10f, .6f); + Run(clear, 2); + Run(cloudy, 2); + clear.Observer.Sensor.Ping(); + cloudy.Observer.Sensor.Ping(); + + var biggestJump = 0f; + var last = Info(clear.Observer, clear.Target); + for (var i = 0; i < 12; i++) + { + Tick(clear); + Tick(cloudy); + var open = Info(clear.Observer, clear.Target); + biggestJump = max(biggestJump, open - last); + last = open; + Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / open, 2); + } + Assert.True(biggestJump > .15f, $"the ping left no mark on the trace ({biggestJump})"); + } + // The chunk path reads the same rule: the passive rate toward a point is scaled by the same survivor fraction. [Fact] public void TheChunkRateIsScaledByTheSameFactor() @@ -346,7 +398,25 @@ public sealed class VapourCloudTests : IDisposable var open = lab.Observer.Sensor.PassiveRate(100f, at); Assert.True(open > 0f); - Hand(lab.Zone, at, 20f, .6f); + Hand(lab.Zone, float2(0, 50), 10f, .6f); Assert.Equal(.4f, lab.Observer.Sensor.PassiveRate(100f, at) / open, 5); } + + // The dump's wire shape: BehaviorData union 42, its radius, opacity and lifetime at keys 1, 2 and 3. + [Fact] + public void TheDumpIsUnion42WithRadiusOpacityLifetimeAtKeys1To3() + { + var options = CultDocumentMessagePackSerialization.OptionsFor(typeof(ZonePack).Assembly); + var data = new VapourDumpData { Radius = Constant(1f), Opacity = Constant(2f), Lifetime = Constant(3f) }; + var bytes = MessagePackSerializer.Serialize(data, options); + + var reader = new MessagePackReader(bytes); + Assert.Equal(2, reader.ReadArrayHeader()); + Assert.Equal(42, reader.ReadInt32()); + Assert.Equal(4, reader.ReadArrayHeader()); + reader.Skip(); + Assert.Equal(1f, MessagePackSerializer.Deserialize(ref reader, options).Min); + Assert.Equal(2f, MessagePackSerializer.Deserialize(ref reader, options).Min); + Assert.Equal(3f, MessagePackSerializer.Deserialize(ref reader, options).Min); + } } From f79cf8b7a77dfb742f55592033a47c67ec1d3284 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:29:27 +0200 Subject: [PATCH 10/49] ConsumableTests: a hold with room for two consumables The 1x1 interior fixture could not store the second consumable the non-stackable test needs. Builds. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ConsumableTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs index 885e4e111..740f739ff 100644 --- a/tests/Aetheria.Shared.Tests/ConsumableTests.cs +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -54,7 +54,7 @@ public sealed class ConsumableTests : IDisposable Name = "Engine", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 10, Behaviors = { new ThrusterData { Thrust = thrust } } }); - var interior = new Shape(1, 1); + var interior = new Shape(2, 1); // room for two consumables foreach (var cell in interior.AllCoordinates) interior[cell] = true; var holdRef = cache.Upsert(new CargoBayData { Name = "Hold", Shape = new Shape(), InteriorShape = interior, Durability = 10 }); var consumableRef = cache.Upsert(new ConsumableItemData From 0c29ea1768ecb8f43fe992e3fbc7cbab7dd32db3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:32:00 +0200 Subject: [PATCH 11/49] ThrottleLockTests: place thrusters on interior hull cells The first commit's tests failed in setup (border cells are not interior); it builds, its three new tests were red. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ThrottleLockTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs index 1673e2be1..db2ad9361 100644 --- a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -75,8 +75,8 @@ public sealed class ThrottleLockTests : IDisposable var ship = new Ship(items, zone, Mint(cache, items, cache.GetByName("Skiff")), new EntitySettings()); var gear = cache.GetByName("Thruster"); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Reversed), new int2(2, 1))); - Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(0, 2))); - Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(4, 2))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(1, 2))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(3, 2))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(3, 3))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(1, 3))); zone.Entities.Add(ship); From 2b03d908127ffda1f640daf9f70b6792284cea59 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:35:37 +0200 Subject: [PATCH 12/49] RoundAt clamps at the weapon's frozen Range (PendingShot.MaxRange), not FireRange FireRange is the distance to the engaged target and 0 for a round with none, so a no-lock burst round and a round that misses never flew. MaxRange = weapon.Range is frozen at Fire beside Speed. Unity compile not yet run. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Gameplay/Weapons/Projectile.cs | 4 ++-- Assets/Scripts/ServerShared/FireControl.cs | 8 ++++++-- tests/Aetheria.Shared.Tests/BallisticFlightTests.cs | 8 ++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Assets/Scripts/Gameplay/Weapons/Projectile.cs b/Assets/Scripts/Gameplay/Weapons/Projectile.cs index 4e38ad549..64d84fb4e 100644 --- a/Assets/Scripts/Gameplay/Weapons/Projectile.cs +++ b/Assets/Scripts/Gameplay/Weapons/Projectile.cs @@ -8,7 +8,7 @@ using static CultMath.math; // (origin, direction, speed, fire time, arrival, range: the PendingShot copy taken at spawn), so this object stores no // velocity or position of its own and integrates nothing: each frame it asks RoundAt where the round is at sim time // plus the clock's Lead (the same draw-ahead ships use), and the simulation's ShotResolved ends a round that hit or -// burst. A round that missed flies on to the FireRange point RoundAt clamps at, and is done there. The barrel is only +// burst. A round that missed flies on to the MaxRange point RoundAt clamps at, and is done there. The barrel is only // where the round is first seen: its offset from the sim line decays to zero over BlendTime sim seconds. The trail's // fade after the end stays real time (pure presentation, ruling sim-speed-presentation). public class Projectile : MonoBehaviour @@ -50,7 +50,7 @@ public class Projectile : MonoBehaviour if (!_alive) return; var time = _zone.Time + ActionGameManager.Clock.Lead; transform.position = At(FireControl.RoundAt(_shot, time), time); - var end = _shot.Speed > .01f ? _shot.FireTime + _shot.FireRange / _shot.Speed : _shot.FireTime; + var end = _shot.Speed > .01f ? _shot.FireTime + _shot.MaxRange / _shot.Speed : _shot.FireTime; if (time >= end) Finish(); } diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index 097c3cc79..40e3e25a8 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -515,12 +515,12 @@ public static class FireControl } // Where a ballistic round is at a sim time: the one statement of the round's line, from its frozen origin along its - // frozen direction at its frozen speed, held at the FireRange point once it has flown that far. Pure: it reads the + // frozen direction at its frozen speed, held at the MaxRange point once it has flown that far. Pure: it reads the // shot alone, and the simulation's own arithmetic (Step, Commit, Apply) does not call it. Presenters draw rounds here. public static float2 RoundAt(in PendingShot shot, float time) { if (shot.Speed <= .01f) return shot.FireOrigin.xz; - var flown = clamp(time - shot.FireTime, 0f, shot.FireRange / shot.Speed); + var flown = clamp(time - shot.FireTime, 0f, shot.MaxRange / shot.Speed); return shot.FireOrigin.xz + shot.TravelDirection * (shot.Speed * flown); } @@ -766,6 +766,7 @@ public static class FireControl FireRange = fireRange, FireTime = now, Speed = weapon.Velocity, + MaxRange = weapon.Range, FireOrigin = origin, FireTargetPosition = targetPosition, FireTargetVelocity = targetVelocity, @@ -2012,6 +2013,9 @@ public struct PendingShot public float FireTime; // The weapon's Velocity at Fire: frozen with the rest, so RoundAt states the same line whatever happens to the stat later. public float Speed; + // The weapon's Range at Fire, likewise frozen: how far the round can fly, which RoundAt clamps at. FireRange above is the + // distance to the engaged target (0 for a round with none), not how far a round that misses flies on. + public float MaxRange; public float CommitTime; public float ArrivalTime; diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index cd3582d70..c9589a0db 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -48,7 +48,7 @@ public sealed partial class FireControlCut124Tests Assert.Equal(burst.BurstPosition.x, arrival.x, 3); Assert.Equal(burst.BurstPosition.z, arrival.y, 3); - // Past the range it is held at the FireRange point, however late it is asked. + // Past the weapon's range it is held at the range point, however late it is asked. var held = FireControl.RoundAt(burst, burst.FireTime + NoLockRange / BallisticSpeed + 50f); Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); Assert.Equal(origin.z + aim.y * NoLockRange, held.y, 3); @@ -57,7 +57,7 @@ public sealed partial class FireControlCut124Tests Assert.Equal(origin.x, before.x, 3); Assert.Equal(origin.z, before.y, 3); - // Direct round at a stationary target 100 ahead (+z): arrives on the target, and holds at the range point. + // Direct round at a stationary target 100 ahead (+z): arrives on the target, and a miss would fly on to the weapon's range. var d = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, weaponRange: 1000f); var dOrigin = d.Shooter.Position; FireControl.Fire(d.Weapon, d.WeaponItem, d.Shooter); @@ -66,8 +66,8 @@ public sealed partial class FireControlCut124Tests Assert.Equal(d.Target.Position.x, hit.x, 2); Assert.Equal(d.Target.Position.z, hit.y, 2); Assert.Equal(dOrigin.z + 100f / 2f, FireControl.RoundAt(direct, direct.FireTime + 2.5f).y, 2); - var heldDirect = FireControl.RoundAt(direct, direct.FireTime + direct.FireRange / BallisticSpeed + 99f); - Assert.Equal(dOrigin.z + direct.FireRange, heldDirect.y, 2); + var heldDirect = FireControl.RoundAt(direct, direct.FireTime + 1000f / BallisticSpeed + 99f); + Assert.Equal(dOrigin.z + 1000f, heldDirect.y, 2); } // A round with no speed (the weapon authors Velocity 0, or effectively 0) does not fly: it is at its origin at any From 42c403dc326960f00c6affc75c69ba7311eb2506 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:39:24 +0200 Subject: [PATCH 13/49] VapourCloudTests: settled-state ratios, drag-aware drift, capture at vent Fixes the fixtures the first verify run falsified: the sensor's start-up seed made one-tick ratios meaningless (compare settled infos and the settled ping extra), launch drag moves a cloud less than its velocity, and the vent's own velocity is read when the cloud is added, not after it has drifted. Test-only; builds. Co-Authored-By: Claude Opus 5.5 --- .../Aetheria.Shared.Tests/VapourCloudTests.cs | 77 +++++++++++-------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs index c2300264e..08f24848a 100644 --- a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs +++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs @@ -85,7 +85,7 @@ public sealed class VapourCloudTests : IDisposable { Sensitivity = Constant(1f), SensitivityCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }, - PingBoost = Constant(.00002f), PingEnergy = Constant(0f), PingVisibility = Constant(5f), + PingBoost = Constant(.000004f), PingEnergy = Constant(0f), PingVisibility = Constant(5f), PingRange = Constant(300f), PingCooldown = Constant(4f) })); cache.Upsert(Gear("Vent", HardpointType.Tool, new VapourDumpData @@ -238,7 +238,8 @@ public sealed class VapourCloudTests : IDisposable Assert.True(Info(lab.Target, lab.Observer) < Threshold); Assert.DoesNotContain(lab.Observer, lab.Target.VisibleEnemies); - Assert.Equal(Info(lab.Observer, lab.Target), Info(lab.Target, lab.Observer), 5); + // (The observer updates before the venter in the tick that vents, so it is one tick behind.) + Assert.Equal(Info(lab.Observer, lab.Target), Info(lab.Target, lab.Observer), 3); } [Fact] @@ -272,9 +273,10 @@ public sealed class VapourCloudTests : IDisposable lab.Zone.Update(1f); - Assert.Equal(10f, cloud.Body.Position.x, 3); + // The launch velocity decays on the zone's launch drag, so it has moved most of the way and no further. + Assert.InRange(cloud.Body.Position.x, 5f, 9.9f); Assert.Equal(1f, lab.Zone.Obscuration(float2(0, -20), float2(0, 20))); - Assert.Equal(.5f, lab.Zone.Obscuration(float2(10, -20), float2(10, 20)), 2); + Assert.Equal(.5f, lab.Zone.Obscuration(float2(cloud.Body.Position.x, -20), float2(cloud.Body.Position.x, 20)), 2); } [Fact] @@ -296,9 +298,13 @@ public sealed class VapourCloudTests : IDisposable VapourCloud vented = null; float2 venterVelocity = float2.zero; float2 venterAt = float2.zero; + float2 cloudVelocity = float2.zero; + float2 cloudAt = float2.zero; using var sub = lab.Zone.Clouds.ObserveAdd().Subscribe(add => { vented = add.Value; + cloudVelocity = add.Value.Body.Velocity; + cloudAt = add.Value.Body.Position; venterVelocity = lab.Target.Velocity; venterAt = lab.Target.Position.xz; }); @@ -309,8 +315,8 @@ public sealed class VapourCloudTests : IDisposable Assert.Single(lab.Zone.Clouds); Assert.Same(vented, lab.Zone.Clouds.Single()); Assert.True(length(venterVelocity) > 0f, "the venter was not moving"); - Assert.Equal(venterVelocity, vented.Body.Velocity); - Assert.Equal(venterAt, vented.Body.Position); + Assert.Equal(venterVelocity, cloudVelocity); + Assert.Equal(venterAt, cloudAt); Assert.Same(lab.Target, vented.Venter); // Each stat is the authored one, evaluated through the host. Assert.Equal(17f, vented.Radius); @@ -345,48 +351,51 @@ public sealed class VapourCloudTests : IDisposable Assert.Equal(.4f, Through(float2(-50, 0), float2(15, 0)), 5); } - // Where the rule is decided: one tick from nothing, the info gathered is exactly what survives the cloud - // times what the open sight line gathers, in both directions. + // Where the rule is decided: once the start-up seed has decayed away, the info a sensor settles at is what + // survives the cloud times what the open sight line settles at, in both directions. [Fact] public void TheGainIsScaledByWhatSurvivesTheCloud() { - var clear = Build(100f); - Tick(clear); - var cloudy = Build(100f); + var clear = Build(400f); + var cloudy = Build(400f); // Mid-way between them and clear of both: only the segment, not either end, touches it. - Hand(cloudy.Zone, float2(0, 50), 10f, .6f); - Tick(cloudy); + Hand(cloudy.Zone, float2(0, 200), 10f, .6f, lifetime: 1e6f); + Run(clear, 400); + Run(cloudy, 400); - Assert.True(Info(clear.Observer, clear.Target) > 0f); + Assert.True(Info(clear.Observer, clear.Target) > Threshold); Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / Info(clear.Observer, clear.Target), 3); Assert.Equal(.4f, Info(cloudy.Target, cloudy.Observer) / Info(clear.Target, clear.Observer), 3); } - // A ping is gathered through the same rule: with a cloud on the sight line the whole trace, ping included, is - // what survives the cloud times the open trace (info is linear in the gain while nothing saturates). + // A ping is gathered through the same rule. Settled, the tick that a ping reaches the target adds the ping gain + // in place of the passive one; that extra, in a lab with a cloud on the sight line, is what survives the + // cloud times the extra in the open (info is linear in the gain while nothing saturates). [Fact] public void ThePingGainIsScaledToo() { - var clear = Build(100f); - var cloudy = Build(100f); - Hand(cloudy.Zone, float2(0, 50), 10f, .6f); - Run(clear, 2); - Run(cloudy, 2); - clear.Observer.Sensor.Ping(); - cloudy.Observer.Sensor.Ping(); - - var biggestJump = 0f; - var last = Info(clear.Observer, clear.Target); - for (var i = 0; i < 12; i++) + float PingExtra(Lab lab) { - Tick(clear); - Tick(cloudy); - var open = Info(clear.Observer, clear.Target); - biggestJump = max(biggestJump, open - last); - last = open; - Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / open, 2); + Run(lab, 400); + lab.Observer.Sensor.Ping(); + var biggest = 0f; + var last = Info(lab.Observer, lab.Target); + for (var i = 0; i < 38; i++) + { + Tick(lab); + biggest = max(biggest, Info(lab.Observer, lab.Target) - last); + last = Info(lab.Observer, lab.Target); + } + return biggest; } - Assert.True(biggestJump > .15f, $"the ping left no mark on the trace ({biggestJump})"); + + var clear = Build(250f); + var cloudy = Build(250f); + Hand(cloudy.Zone, float2(0, 125), 10f, .6f, lifetime: 1e6f); + + var open = PingExtra(clear); + Assert.True(open > .05f, $"the ping left no mark on the open trace ({open})"); + Assert.Equal(.4f, PingExtra(cloudy) / open, 2); } // The chunk path reads the same rule: the passive rate toward a point is scaled by the same survivor fraction. From 84f9cd39e5232937a1ba3dddc22d962f83f35af3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:40:48 +0200 Subject: [PATCH 14/49] VapourCloudTests: settled ratio to two places The settled ratio carries the seed's last residue (.4007 against .4); two places still separates .4 from the squared .16. Test-only; builds. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/VapourCloudTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs index 08f24848a..71fc3cc2a 100644 --- a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs +++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs @@ -364,8 +364,8 @@ public sealed class VapourCloudTests : IDisposable Run(cloudy, 400); Assert.True(Info(clear.Observer, clear.Target) > Threshold); - Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / Info(clear.Observer, clear.Target), 3); - Assert.Equal(.4f, Info(cloudy.Target, cloudy.Observer) / Info(clear.Target, clear.Observer), 3); + Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / Info(clear.Observer, clear.Target), 2); + Assert.Equal(.4f, Info(cloudy.Target, cloudy.Observer) / Info(clear.Target, clear.Observer), 2); } // A ping is gathered through the same rule. Settled, the tick that a ping reaches the target adds the ping gain From 015e6d86bd2abbf68b4d9627689d62a155c39918 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:51:30 +0200 Subject: [PATCH 15/49] ThrottleLockTests: strafe thrusters in mirrored pairs A lone off-axis strafe thruster cancels its own torque in Ship.Update, so its axis read 0 unlocked; pairs leave the strafe axis plain. The previous commit's AnUnlockedShipFollowsIntent was red for this. Co-Authored-By: Claude Opus 5.5 --- .../ThrottleLockTests.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs index db2ad9361..9a1b7ac6d 100644 --- a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -41,7 +41,8 @@ public sealed class ThrottleLockTests : IDisposable public ItemManager Items; public Ship Ship; public ConsumableItemData Lock; - public Thruster Forward, Reverse, Right, Left, Clockwise; + public Thruster Forward, Reverse, Clockwise; + public Thruster[] Right, Left; // pairs on either side of the centre line: a lone off-axis strafe thruster cancels its own torque public void ActivateLock() => Ship.ActivateConsumable(new ConsumableItem { @@ -77,8 +78,10 @@ public sealed class ThrottleLockTests : IDisposable Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Reversed), new int2(2, 1))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(1, 2))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(3, 2))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(1, 3))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(3, 3))); - Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(1, 3))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(1, 1))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(3, 1))); zone.Entities.Add(ship); ship.Aim = float3(0, 0, 1); ship.Activate(); @@ -88,8 +91,8 @@ public sealed class ThrottleLockTests : IDisposable { Cache = cache, Items = items, Ship = ship, Lock = cache.GetByName("Overdrive"), Forward = thrusters.Single(t => t.Item.EquippableItem.Rotation == ItemRotation.Reversed), - Right = thrusters.Single(t => t.Item.EquippableItem.Rotation == ItemRotation.CounterClockwise), - Left = thrusters.Single(t => t.Item.EquippableItem.Rotation == ItemRotation.Clockwise), + Right = thrusters.Where(t => t.Item.EquippableItem.Rotation == ItemRotation.CounterClockwise).ToArray(), + Left = thrusters.Where(t => t.Item.EquippableItem.Rotation == ItemRotation.Clockwise).ToArray(), Reverse = thrusters.First(t => t.Item.EquippableItem.Rotation == ItemRotation.None && t.Torque <= 0), Clockwise = thrusters.First(t => t.Item.EquippableItem.Rotation == ItemRotation.None && t.Torque > 0) }; @@ -112,8 +115,8 @@ public sealed class ThrottleLockTests : IDisposable f.Ship.Update(0.01f); Assert.Equal(1f, f.Forward.Axis); Assert.Equal(0f, f.Reverse.Axis); - Assert.Equal(0f, f.Right.Axis); - Assert.Equal(0f, f.Left.Axis); + Assert.All(f.Right, t => Assert.Equal(0f, t.Axis)); + Assert.All(f.Left, t => Assert.Equal(0f, t.Axis)); } // Turning stays with the pilot: the clockwise-torque thruster follows Turn while the lock holds. @@ -162,10 +165,15 @@ public sealed class ThrottleLockTests : IDisposable f.Ship.MovementDirection = float2(1, 0); f.Ship.Update(0.01f); - Assert.Equal(1f, f.Right.Axis); - Assert.Equal(0f, f.Left.Axis); + Assert.All(f.Right, t => Assert.Equal(1f, t.Axis)); + Assert.All(f.Left, t => Assert.Equal(0f, t.Axis)); Assert.Equal(0f, f.Forward.Axis); + f.Ship.MovementDirection = float2(-1, 0); + f.Ship.Update(0.01f); + Assert.All(f.Left, t => Assert.Equal(1f, t.Axis)); + Assert.All(f.Right, t => Assert.Equal(0f, t.Axis)); + f.Ship.MovementDirection = float2(0, 1); f.Ship.Update(0.01f); Assert.Equal(1f, f.Forward.Axis); From 8c549719c0ce74f5a4518cb2c1e10bbaf3cdcbf5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:54:20 +0200 Subject: [PATCH 16/49] ThrottleLockTests: strafe pairs placed for opposing torque Pairs mirrored across the ship's width had equal torque and the compensation still cancelled one; the previous commit's AnUnlockedShipFollowsIntent was red. Mirror across its length instead. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ThrottleLockTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs index 9a1b7ac6d..03f7b1060 100644 --- a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -42,7 +42,7 @@ public sealed class ThrottleLockTests : IDisposable public Ship Ship; public ConsumableItemData Lock; public Thruster Forward, Reverse, Clockwise; - public Thruster[] Right, Left; // pairs on either side of the centre line: a lone off-axis strafe thruster cancels its own torque + public Thruster[] Right, Left; // pairs with opposing torque: a lone off-axis strafe thruster cancels its own torque public void ActivateLock() => Ship.ActivateConsumable(new ConsumableItem { @@ -78,10 +78,11 @@ public sealed class ThrottleLockTests : IDisposable Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Reversed), new int2(2, 1))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(1, 2))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.None), new int2(3, 2))); + // Strafe pairs sit either side of the centre line across the ship's length, so their torques oppose. + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(1, 1))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(1, 3))); - Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(3, 3))); - Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(1, 1))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(3, 1))); + Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(3, 3))); zone.Entities.Add(ship); ship.Aim = float3(0, 0, 1); ship.Activate(); From e56f9c8be52ec340fb5c499b338a801b318a445c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:56:01 +0200 Subject: [PATCH 17/49] BallisticFlightTests: move the shooter after Fire so a live-origin RoundAt fails The origin-from-live-shooter hand mutant survived against a stationary shooter. Builds: yes. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/BallisticFlightTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index c9589a0db..3468a3034 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -35,6 +35,8 @@ public sealed partial class FireControlCut124Tests var e = FireBurstRound(out var origin); var burst = SafeAssert.OnlyShot(e.Zone); var aim = normalize(float2(2, 1)); + // The shooter moves on after the round left: the line is the one frozen at Fire, not one drawn from where it is now. + e.Shooter.Position += float3(7, 0, 3); var start = FireControl.RoundAt(burst, burst.FireTime); Assert.Equal(origin.x, start.x, 3); From 950b0e3ddcdfabae3a311bbc4a0f08f08f12ce48 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 21:17:35 +0200 Subject: [PATCH 18/49] VapourCloud: drop the zero-lifetime guard saturate already covers The mutation table let the guard's removal survive: with saturate's NaN rule (max(0, NaN) = 0) a zero-lifetime cloud has no opacity without it, and ACloudWithNoLifetimeObscuresNothing still pins that behaviour. Builds. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/VapourClouds.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Assets/Scripts/ServerShared/VapourClouds.cs b/Assets/Scripts/ServerShared/VapourClouds.cs index d6d1aaa24..9f4a44603 100644 --- a/Assets/Scripts/ServerShared/VapourClouds.cs +++ b/Assets/Scripts/ServerShared/VapourClouds.cs @@ -16,8 +16,9 @@ public sealed class VapourCloud public float Opacity; public Entity Venter; + // A cloud with no lifetime divides by zero; saturate maps the NaN and the infinity to nothing left. public float OpacityAt(double now) => - Lifetime <= 0f ? 0f : saturate(Opacity) * saturate(1f - (float) (now - VentedAt) / Lifetime); + saturate(Opacity) * saturate(1f - (float) (now - VentedAt) / Lifetime); // Whether the segment a-b reaches the disc: the closest point of the segment to the centre lies within the // radius. A cloud past either end of the segment touches it only if it is near that end. From 6d83597b77673e91a7bb51ab5d5ba4952c779a22 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:27:59 +0200 Subject: [PATCH 19/49] Map: ballistic-flight r2 body facts and why a round's end is one sim statement Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index 3cd61747c..b095dc870 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -5835,6 +5835,15 @@ factor), which waits on all of them. a smaller step means more decisions per sim second, the same per real second. No ruling fixes a per-step count. +**Body facts for ballistic-flight r2** (source reads at e56f9c8b, 2026-10-09, imagination-aeth-ballistic-r2): + +- B-r2-1. FireControl.Step writes a committed shot back (`shots[i] = shot`, FireControl.cs:868) before `ShotCommitted` fires (:869). A no-lock fused round stopped by a hull has its `ArrivalTime` shortened at that commit (:867; CommitBurst :1024). A presenter's fire-time copy of the shot therefore carries a stale arrival unless it re-reads the record at `ShotCommitted`. +- B-r2-2. `FireControl.Designated` measures `range` to any non-null target before the in-range test (:293-297), and a direct round's FlightDistance is that range. A direct round can therefore be fired at a target beyond Range and arrive after `FireTime + MaxRange/Speed`. +- B-r2-3. `InstantWeapon.Execute` calls `FireControl.Fire` and then `OnFire` (:271-272) inside the entity loop, and `FireControl.Step` runs after it (Zone.cs:232). A presenter spawned from `OnFire` binds `ShotCommitted` and `ShotResolved` before its shot can commit or resolve, including a round fired at nothing that commits and misses in the same step. +- B-r2-4. Solve aims a direct or engaged fused round at the predicted intercept (`Solution().Direction`, :135-156) but sets FlightDistance to the current planar range (:654-666), so on a moving target the arrival point on the frozen line is not where the target is. Open as question direct-arrival-at-intercept. + +**Why a round's end is one sim statement.** The sim resolves a round at `ArrivalTime`. A drawn round that ends anywhere else needs a second owner of the end, which is what ballistic-flight s1 found in Projectile. `FireControl.RoundEnd(shot, known)` states the end for what the sim has published about the outcome: arrival for a hit, a burst or an unknown outcome, and the frozen weapon range for a miss, never short of arrival. `RoundAt` clamps there. A round whose outcome is not yet published waits at its arrival point for at most one step instead of overshooting. The drawing rules (placement, barrel blend, stop, done) are pure statics in DrawAhead, beside the ships' draw-ahead, so the headless suite tests them by behaviour rather than by grep. + ### Census Every Unity-side place that integrates, times or decides a sim fact at `6d427b7a`, and the cut From 60aacfdc358c7a706b9020e0c5fa1c1c738884a3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:31:12 +0200 Subject: [PATCH 20/49] FireControl.RoundEnd states a round's end; DrawAhead draws rounds as pure statics RoundEnd(shot, known) is the one statement of when a ballistic round's drawn flight ends: arrival for a hit, burst or unpublished outcome, the frozen range (never short of arrival) for a miss. RoundAt takes the known outcome and clamps at RoundEnd. DrawAhead gains Round, RoundStop, RoundOver (pure, no state) so the blend, stop and done rules are headless-testable. Seven new tests (ten cases). Unity build: Projectile.cs still calls the old RoundAt, so this commit does not compile in Unity (the headless test project does not include Projectile). The next commit moves Projectile onto the statics. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/DrawAhead.cs | 25 +- Assets/Scripts/ServerShared/FireControl.cs | 24 +- .../BallisticFlightTests.cs | 290 ++++++++++++++++-- 3 files changed, 313 insertions(+), 26 deletions(-) diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index d26bf91b4..77365cebe 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -5,8 +5,9 @@ using CultMath; using static CultMath.math; -// Where a presenter draws the latest sim state when the frame falls Lead sim seconds after the last step. Pure reads -// of the simulation for presenters (never called by ServerShared): the sim owns every pose and no presenter stores +// Where a presenter draws the latest sim state when the frame falls Lead sim seconds after the last step: ships by +// their last step's velocity, ballistic rounds on FireControl's RoundAt and RoundEnd. Pure reads of the simulation for +// presenters (never called by ServerShared, no state of their own): the sim owns every pose and no presenter stores // one, a previous state or a velocity of its own. public static class DrawAhead { @@ -37,4 +38,24 @@ public static class DrawAhead t.w * q.z + t.x * q.y - t.y * q.x + t.z * q.w, t.w * q.w - t.x * q.x - t.y * q.y - t.z * q.z)); } + + // A ballistic round at a sim time (the clock's Zone.Time + Lead): on FireControl's line for what the sim has published + // of its outcome, with the barrel's offset from that line decaying to nothing over blendTime sim seconds from FireTime. + public static float3 Round(in PendingShot shot, ShotResult? known, float3 barrel, float blendTime, float time) => + Lifted(shot, FireControl.RoundAt(shot, known, time), barrel, blendTime, time); + + // Where a round that hit or burst stops: the outcome's burst point, else where it arrives on its line. + public static float3 RoundStop(in PendingShot shot, ShotOutcome outcome, float3 barrel, float blendTime) => + Lifted(shot, outcome.HasBurstPoint ? outcome.BurstPoint : FireControl.RoundAt(shot, outcome.Result, shot.ArrivalTime), + barrel, blendTime, shot.ArrivalTime); + + // Whether the drawn flight is over by time: only a published miss ends by the clock. A hit or burst ends on ShotResolved. + public static bool RoundOver(in PendingShot shot, ShotResult? known, float time) => + known == ShotResult.Miss && time >= FireControl.RoundEnd(shot, ShotResult.Miss); + + private static float3 Lifted(in PendingShot shot, float2 planar, float3 barrel, float blendTime, float time) + { + var blend = blendTime > 0 ? saturate(1 - (time - shot.FireTime) / blendTime) : 0f; + return float3(planar.x, shot.FireOrigin.y, planar.y) + (barrel - shot.FireOrigin) * blend; + } } diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index 40e3e25a8..c1c4e8ddd 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -514,13 +514,23 @@ public static class FireControl return shot.Target == null ? 1f : PDeviation(deviation, shot.Tracking); } + // The sim time a ballistic round's drawn flight ends, for what is known of its outcome: the one statement of a round's + // end. A round that hit or burst (or whose outcome is not yet published) ends at its ArrivalTime. A round the sim has + // published as a Miss flies on to the weapon's frozen range and never ends short of its own arrival (a direct round's + // FireRange is measured to any target, in range or not). Pure: it reads the shot alone, and the simulation's own + // arithmetic (Step, Commit, Apply) does not call it. Presenters end rounds here, through DrawAhead. + public static float RoundEnd(in PendingShot shot, ShotResult? known) => + known == ShotResult.Miss + ? max(shot.ArrivalTime, shot.FireTime + (shot.Speed > .01f ? shot.MaxRange / shot.Speed : 0f)) + : shot.ArrivalTime; + // Where a ballistic round is at a sim time: the one statement of the round's line, from its frozen origin along its - // frozen direction at its frozen speed, held at the MaxRange point once it has flown that far. Pure: it reads the - // shot alone, and the simulation's own arithmetic (Step, Commit, Apply) does not call it. Presenters draw rounds here. - public static float2 RoundAt(in PendingShot shot, float time) + // frozen direction at its frozen speed, held at RoundEnd once it has flown that far. With no outcome known the round is + // drawn no further than its arrival point and waits there for the sim to publish one. Pure, like RoundEnd. + public static float2 RoundAt(in PendingShot shot, ShotResult? known, float time) { if (shot.Speed <= .01f) return shot.FireOrigin.xz; - var flown = clamp(time - shot.FireTime, 0f, shot.MaxRange / shot.Speed); + var flown = clamp(time - shot.FireTime, 0f, RoundEnd(shot, known) - shot.FireTime); return shot.FireOrigin.xz + shot.TravelDirection * (shot.Speed * flown); } @@ -865,6 +875,8 @@ public static class FireControl // A contact burst short of max range arrives when the round has flown that far, not at the frozen // max-range time (CommitBurst). Never later than the frozen time. if (shot.Outcome.Result == ShotResult.Burst) shot.ArrivalTime = min(shot.ArrivalTime, now + shot.Outcome.ArrivalIn); + // Written back before ShotCommitted fires: a presenter re-reading the record there (Zone.TryGetShot) sees + // the committed ArrivalTime a contact burst has just shortened. shots[i] = shot; zone.ShotCommitted.OnNext(shot.Outcome); } @@ -2013,8 +2025,8 @@ public struct PendingShot public float FireTime; // The weapon's Velocity at Fire: frozen with the rest, so RoundAt states the same line whatever happens to the stat later. public float Speed; - // The weapon's Range at Fire, likewise frozen: how far the round can fly, which RoundAt clamps at. FireRange above is the - // distance to the engaged target (0 for a round with none), not how far a round that misses flies on. + // The weapon's Range at Fire, likewise frozen: how far a round that misses flies on, read only by RoundEnd. FireRange above + // is the distance to the engaged target (0 for a round with none), not how far a round that misses flies on. public float MaxRange; public float CommitTime; public float ArrivalTime; diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index 3468a3034..0781d2c06 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -9,9 +9,11 @@ using float2 = CultMath.float2; using float3 = CultMath.float3; // Ballistic flight: FireControl.RoundAt is the one statement of where a fired round is at a sim time, from the -// PendingShot's frozen origin, direction, speed and fire time. Unity draws rounds there and integrates nothing, so -// these tests pin the line itself against first principles (the shooter's position, the aim, the authored velocity) -// rather than against the code that computes it. Shares the Cut 12.4 engagement fixture. +// PendingShot's frozen origin, direction, speed and fire time, and FireControl.RoundEnd the one statement of when its +// drawn flight ends for what the sim has published of its outcome. Unity draws rounds through DrawAhead's round statics +// and integrates nothing, so these tests pin the line, the end and the drawing against first principles (the shooter's +// position, the aim, the authored velocity and range) rather than against the code that computes them. Shares the +// Cut 12.4 engagement fixture. public sealed partial class FireControlCut124Tests { private const float BallisticSpeed = 20f; @@ -38,24 +40,28 @@ public sealed partial class FireControlCut124Tests // The shooter moves on after the round left: the line is the one frozen at Fire, not one drawn from where it is now. e.Shooter.Position += float3(7, 0, 3); - var start = FireControl.RoundAt(burst, burst.FireTime); + var start = FireControl.RoundAt(burst, null, burst.FireTime); Assert.Equal(origin.x, start.x, 3); Assert.Equal(origin.z, start.y, 3); - var second = FireControl.RoundAt(burst, burst.FireTime + 1f); + var second = FireControl.RoundAt(burst, null, burst.FireTime + 1f); Assert.Equal(origin.x + aim.x * BallisticSpeed, second.x, 3); Assert.Equal(origin.z + aim.y * BallisticSpeed, second.y, 3); - var arrival = FireControl.RoundAt(burst, burst.ArrivalTime); - Assert.Equal(burst.BurstPosition.x, arrival.x, 3); - Assert.Equal(burst.BurstPosition.z, arrival.y, 3); + foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Burst }) + foreach (var later in new[] { 0f, 1f, 50f }) + { + var arrival = FireControl.RoundAt(burst, known, burst.ArrivalTime + later); + Assert.Equal(burst.BurstPosition.x, arrival.x, 3); + Assert.Equal(burst.BurstPosition.z, arrival.y, 3); + } // Past the weapon's range it is held at the range point, however late it is asked. - var held = FireControl.RoundAt(burst, burst.FireTime + NoLockRange / BallisticSpeed + 50f); + var held = FireControl.RoundAt(burst, ShotResult.Miss, burst.FireTime + NoLockRange / BallisticSpeed + 50f); Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); Assert.Equal(origin.z + aim.y * NoLockRange, held.y, 3); // Before it was fired it is still at the origin, not behind it. - var before = FireControl.RoundAt(burst, burst.FireTime - 5f); + var before = FireControl.RoundAt(burst, null, burst.FireTime - 5f); Assert.Equal(origin.x, before.x, 3); Assert.Equal(origin.z, before.y, 3); @@ -64,11 +70,16 @@ public sealed partial class FireControlCut124Tests var dOrigin = d.Shooter.Position; FireControl.Fire(d.Weapon, d.WeaponItem, d.Shooter); var direct = SafeAssert.OnlyShot(d.Zone); - var hit = FireControl.RoundAt(direct, direct.ArrivalTime); - Assert.Equal(d.Target.Position.x, hit.x, 2); - Assert.Equal(d.Target.Position.z, hit.y, 2); - Assert.Equal(dOrigin.z + 100f / 2f, FireControl.RoundAt(direct, direct.FireTime + 2.5f).y, 2); - var heldDirect = FireControl.RoundAt(direct, direct.FireTime + 1000f / BallisticSpeed + 99f); + foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Hit }) + // A round not known to miss waits at its arrival point however late it is asked. + foreach (var at in new[] { direct.ArrivalTime, direct.FireTime + 99f }) + { + var hit = FireControl.RoundAt(direct, known, at); + Assert.Equal(d.Target.Position.x, hit.x, 2); + Assert.Equal(d.Target.Position.z, hit.y, 2); + } + Assert.Equal(dOrigin.z + 100f / 2f, FireControl.RoundAt(direct, null, direct.FireTime + 2.5f).y, 2); + var heldDirect = FireControl.RoundAt(direct, ShotResult.Miss, direct.FireTime + 1000f / BallisticSpeed + 99f); Assert.Equal(dOrigin.z + 1000f, heldDirect.y, 2); } @@ -82,7 +93,7 @@ public sealed partial class FireControlCut124Tests FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); var shot = SafeAssert.OnlyShot(e.Zone); - var late = FireControl.RoundAt(shot, shot.FireTime + 5000f); + var late = FireControl.RoundAt(shot, ShotResult.Miss, shot.FireTime + 5000f); Assert.Equal(origin.x, late.x, 3); Assert.Equal(origin.z, late.y, 3); } @@ -102,7 +113,7 @@ public sealed partial class FireControlCut124Tests var after = SafeAssert.OnlyShot(e.Zone); Assert.Equal(BallisticSpeed, after.Speed, 4); var aim = normalize(float2(2, 1)); - var at = FireControl.RoundAt(after, after.FireTime + 1f); + var at = FireControl.RoundAt(after, null, after.FireTime + 1f); Assert.Equal(origin.x + aim.x * BallisticSpeed, at.x, 3); Assert.Equal(origin.z + aim.y * BallisticSpeed, at.y, 3); } @@ -117,8 +128,18 @@ public sealed partial class FireControlCut124Tests var positions = entities.Select(x => x.Position).ToList(); var velocities = entities.Select(x => x.Velocity).ToList(); + var barrel = shot.FireOrigin + float3(1, .5f, -2); for (var i = 0; i < 25; i++) - FireControl.RoundAt(shot, shot.FireTime + i * .37f); + { + var at = shot.FireTime + i * .37f; + foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Miss }) + { + FireControl.RoundAt(shot, known, at); + FireControl.RoundEnd(shot, known); + DrawAhead.Round(shot, known, barrel, .1f, at); + DrawAhead.RoundOver(shot, known, at); + } + } Assert.Equal(time, e.Zone.Time); Assert.True(e.Zone.PendingShots.Count == 1 && e.Zone.PendingShots[0].Equals(shot), "the pending shot changed"); @@ -126,4 +147,237 @@ public sealed partial class FireControlCut124Tests Assert.True(entities.Select(x => x.Position).SequenceEqual(positions), "an entity moved"); Assert.True(entities.Select(x => x.Velocity).SequenceEqual(velocities), "an entity's velocity changed"); } + + // The weapon's Range is frozen with the rest: a round that misses flies on to the range it left with, whatever the stat + // becomes after. + [Fact] + public void RangeIsFrozenAtFire() + { + var e = FireBurstRound(out var origin); + var shot = SafeAssert.OnlyShot(e.Zone); + Assert.Equal(NoLockRange, shot.MaxRange, 4); + var endBefore = FireControl.RoundEnd(shot, ShotResult.Miss); + + // The stat changes after the round left, and the weapon re-reads it. + ((InstantWeaponData) e.Weapon.WeaponData).Range = Constant(NoLockRange * 5f); + e.Weapon.Execute(0f); + Assert.Equal(NoLockRange * 5f, e.Weapon.Range, 4); + + var after = SafeAssert.OnlyShot(e.Zone); + Assert.Equal(NoLockRange, after.MaxRange, 4); + Assert.Equal(endBefore, FireControl.RoundEnd(after, ShotResult.Miss), 4); + var aim = normalize(float2(2, 1)); + var held = FireControl.RoundAt(after, ShotResult.Miss, after.FireTime + 1000f); + Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); + Assert.Equal(origin.z + aim.y * NoLockRange, held.y, 3); + } + + // A direct round at a stationary target `targetRange` ahead (+z) of the shooter, weapon Range `range`, at `speed`; + // or, with atNothing, the same weapon fired along +z at no target. + private Engagement FireDirectRound(float speed, float range, float targetRange = 100f, bool atNothing = false) + { + var e = Build(TestSettings(), SolidShape(5, 4), velocity: speed, weaponRange: range, targetRange: targetRange); + if (atNothing) + { + Aim(e, float2(0, 1)); + e.Shooter.SetTarget(TargetRef.None); + } + FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + return e; + } + + // A hit, a burst or an outcome not yet known ends at the round's arrival; a miss flies on to the weapon's range and + // never ends short of its own arrival; only a published miss is over by the clock. + [Fact] + public void RoundEndAndRoundOverAreTheOutcomesEnd() + { + foreach (var speed in new[] { 20f, 200f }) + { + var d = FireDirectRound(speed, 1000f); + var direct = SafeAssert.OnlyShot(d.Zone); + Assert.Equal(direct.FireTime + 100f / speed, direct.ArrivalTime, 4); + Assert.Equal(direct.FireTime + 100f / speed, FireControl.RoundEnd(direct, null), 4); + Assert.Equal(direct.FireTime + 100f / speed, FireControl.RoundEnd(direct, ShotResult.Hit), 4); + Assert.Equal(direct.FireTime + 100f / speed, FireControl.RoundEnd(direct, ShotResult.Burst), 4); + Assert.Equal(direct.FireTime + 1000f / speed, FireControl.RoundEnd(direct, ShotResult.Miss), 4); + + foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Hit, ShotResult.Burst }) + Assert.False(DrawAhead.RoundOver(direct, known, direct.FireTime + 10000f)); + var missEnd = direct.FireTime + 1000f / speed; + Assert.False(DrawAhead.RoundOver(direct, ShotResult.Miss, missEnd - .01f)); + Assert.True(DrawAhead.RoundOver(direct, ShotResult.Miss, missEnd)); + + // A target beyond the weapon's range: a miss never ends short of its own arrival. + var far = FireDirectRound(speed, 100f, targetRange: 150f); + var beyond = SafeAssert.OnlyShot(far.Zone); + Assert.Equal(beyond.FireTime + 150f / speed, beyond.ArrivalTime, 4); + Assert.Equal(beyond.FireTime + 150f / speed, FireControl.RoundEnd(beyond, ShotResult.Miss), 4); + + // A round fired at nothing arrives where it left, and a miss flies on to the range. + var none = FireDirectRound(speed, 100f, atNothing: true); + var nothing = SafeAssert.OnlyShot(none.Zone); + Assert.Equal(nothing.FireTime, nothing.ArrivalTime, 4); + Assert.Equal(nothing.FireTime, FireControl.RoundEnd(nothing, null), 4); + Assert.Equal(nothing.FireTime + 100f / speed, FireControl.RoundEnd(nothing, ShotResult.Miss), 4); + + } + } + + // The frame falls Lead (< one step) after the last step, so a hit or burst is drawn at Zone.Time + Lead; it must stay + // on the arrival point until the sim resolves it, and only a published miss flies on. + [Theory] + [InlineData(20f)] + [InlineData(200f)] + public void ARoundIsDrawnNoFurtherThanItsArrivalUntilItMisses(float speed) + { + var e = FireDirectRound(speed, 1000f); + var shot = SafeAssert.OnlyShot(e.Zone); + var origin = shot.FireOrigin; + const float step = 1f / 30f; + + foreach (var lead in new[] { 0f, step / 2f, .999f * step }) + { + var time = shot.ArrivalTime + lead; + foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Hit, ShotResult.Burst }) + { + var drawn = DrawAhead.Round(shot, known, shot.FireOrigin, 0f, time); + Assert.Equal(origin.x, drawn.x, 3); + Assert.Equal(origin.z + 100f, drawn.z, 3); + } + var missed = DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, 0f, time); + Assert.Equal(origin.x, missed.x, 3); + Assert.Equal(origin.z + 100f + lead * speed, missed.z, 3); + } + + for (var time = shot.FireTime; time <= shot.ArrivalTime + 1f; time += 1f / 144f) + { + var drawn = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, time); + Assert.True(drawn.z - origin.z <= 100f + 1e-3f, "the round was drawn past its arrival point"); + } + } + + // A no-lock contact round stopped by a hull short of max range has its arrival shortened at the commit; the + // simulation writes the record back before ShotCommitted fires, so a presenter re-reading it there draws the stop + // at the committed arrival, not the fire-time copy's. + [Fact] + public void TheCommittedRecordCarriesTheCommittedArrival() + { + var e = Build(TestSettings(), SolidShape(5, 4), velocity: 20f, fuse: WeaponFuse.Contact, blastRadius: 4f, damage: 100f, + weaponRange: NoLockRange); + e.Shooter.SetTarget(TargetRef.None); + Aim(e, float2(0, 1)); + AddShip(e, PointAlong(e, float2(0, 1), 25f), 1f, 300); + ShotOutcome outcome = null; + PendingShot record = default; + var found = false; + using var c = e.Zone.ShotCommitted.Subscribe(o => + { + outcome = o; + found = e.Zone.TryGetShot(o.ShotId, out record); + }); + + FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + var fired = SafeAssert.OnlyShot(e.Zone); + Assert.False(fired.Committed); + e.Zone.Update(.01f); + + Assert.True(found, "the committed record was not readable when ShotCommitted fired"); + Assert.True(record.Committed); + Assert.Equal(fired.FireTime + 3f, fired.ArrivalTime, 2); + Assert.Equal(fired.FireTime + 1.05f, record.ArrivalTime, 2); + Assert.True(record.ArrivalTime < fired.ArrivalTime); + + var stop = DrawAhead.RoundStop(record, outcome, record.FireOrigin, 0f); + var expected = outcome.HasBurstPoint + ? outcome.BurstPoint + : record.FireOrigin.xz + record.TravelDirection * (20f * (record.ArrivalTime - record.FireTime)); + Assert.Equal(expected.x, stop.x, 3); + Assert.Equal(expected.y, stop.z, 3); + foreach (var later in new[] { 0f, .5f, 5f }) + { + var drawn = DrawAhead.Round(record, ShotResult.Burst, record.FireOrigin, 0f, record.ArrivalTime + later); + Assert.Equal(expected.x, drawn.x, 3); + Assert.Equal(expected.y, drawn.z, 3); + } + } + + // The barrel is only where the round is first seen: its offset from the sim line decays linearly to nothing over + // blendTime sim seconds from FireTime. + [Theory] + [InlineData(.1f)] + [InlineData(.25f)] + public void TheBarrelBlendDecaysOnSimTime(float blend) + { + var e = FireBurstRound(out var origin); + var shot = SafeAssert.OnlyShot(e.Zone); + var aim = normalize(float2(2, 1)); + var offset = float3(1, .5f, -2); + var barrel = shot.FireOrigin + offset; + float3 Line(float dt) => float3(origin.x + aim.x * BallisticSpeed * dt, origin.y, origin.z + aim.y * BallisticSpeed * dt); + + void Close(float3 want, float3 got) + { + Assert.Equal(want.x, got.x, 3); + Assert.Equal(want.y, got.y, 3); + Assert.Equal(want.z, got.z, 3); + } + + Close(barrel, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime)); + Close(Line(blend / 2f) + offset * .5f, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend / 2f)); + Close(Line(blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend)); + Close(Line(2f * blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + 2f * blend)); + Close(Line(0f), DrawAhead.Round(shot, null, barrel, 0f, shot.FireTime)); + } + + // Placement depends on sim time alone: any history of frames walked to a checkpoint draws what a fresh call draws, which + // is the frozen line at the frozen speed. + [Theory] + [InlineData(20f)] + [InlineData(200f)] + public void ARoundIsAFunctionOfSimTime(float speed) + { + var e = FireDirectRound(speed, 1000f); + var shot = SafeAssert.OnlyShot(e.Zone); + var checkpoints = new[] { .05f, .3f, 1f, 3f }; + var histories = new[] { new[] { 1f / 144f }, new[] { 1f / 30f }, new[] { 1f / 144f, 1f / 30f, 1f / 90f } }; + + foreach (var frames in histories) + { + var elapsed = 0f; + var frame = 0; + foreach (var checkpoint in checkpoints) + { + while (elapsed + 1e-4f < checkpoint) + { + elapsed = min(checkpoint, elapsed + frames[frame++ % frames.Length]); + DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, .1f, shot.FireTime + elapsed); + } + var drawn = DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint); + var fresh = DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint); + Assert.Equal(fresh.z, drawn.z, 3); + Assert.Equal(shot.FireOrigin.z + speed * min(checkpoint, 1000f / speed), drawn.z, 3); + Assert.Equal(shot.FireOrigin.x, drawn.x, 3); + Assert.Equal(shot.FireOrigin.y, drawn.y, 3); + } + } + } + + // Today's sim geometry: the round flies the line frozen at Fire and stops at the arrival point on it, whatever the + // target does afterwards. (Where a leading round's arrival should lie is the sim's question, never Projectile's.) + [Fact] + public void AHitOnAMovingTargetStopsOnItsFrozenLine() + { + var e = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, weaponRange: 1000f); + e.Target.Velocity = float2(30, 0); + FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + var shot = SafeAssert.OnlyShot(e.Zone); + Assert.True(shot.TravelDirection.x > 0f, "the round did not lead the target"); + + e.Target.Position += float3(40, 0, 0); + var stop = DrawAhead.RoundStop(shot, new ShotOutcome { ShotId = shot.ShotId, Result = ShotResult.Hit }, shot.FireOrigin, 0f); + var line = shot.FireOrigin.xz + shot.TravelDirection * (BallisticSpeed * (shot.ArrivalTime - shot.FireTime)); + Assert.Equal(line.x, stop.x, 3); + Assert.Equal(shot.FireOrigin.y, stop.y, 3); + Assert.Equal(line.y, stop.z, 3); + } } From 85e57aef785ce4e1a4338a7ea1e3ab124fe562d5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:32:08 +0200 Subject: [PATCH 21/49] BallisticFlightTests: using System for the Subscribe lambda; DrawAhead comment names no frame clock Builds headless; Unity still on the old Projectile at this commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/DrawAhead.cs | 2 +- tests/Aetheria.Shared.Tests/BallisticFlightTests.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index 77365cebe..f594b33fe 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -40,7 +40,7 @@ public static class DrawAhead } // A ballistic round at a sim time (the clock's Zone.Time + Lead): on FireControl's line for what the sim has published - // of its outcome, with the barrel's offset from that line decaying to nothing over blendTime sim seconds from FireTime. + // of its outcome, with the barrel's offset from that line decaying to nothing over blendTime sim seconds from the moment of firing. public static float3 Round(in PendingShot shot, ShotResult? known, float3 barrel, float blendTime, float time) => Lifted(shot, FireControl.RoundAt(shot, known, time), barrel, blendTime, time); diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index 0781d2c06..e1957e2db 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +using System; using System.Linq; using Xunit; using static CultMath.math; From a9f11964725bf5899aca4008d68d2643b18b6242 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:33:51 +0200 Subject: [PATCH 22/49] BallisticFlightTests: using UniRx for the Subscribe lambda Builds headless; Unity still on the old Projectile at this commit. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/BallisticFlightTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index e1957e2db..5b1b24dd9 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -4,6 +4,7 @@ using System; using System.Linq; +using UniRx; using Xunit; using static CultMath.math; using float2 = CultMath.float2; From f9483181f2680d732d6d28b49b6b8e15af26aa09 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:35:37 +0200 Subject: [PATCH 23/49] BallisticFlightTests: the moving target crosses at 10, below the round's speed, so an intercept exists Spec r2 said 30 against a round speed of 20: no first-order intercept exists there (the target outruns the round sideways), so the round did not lead. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/BallisticFlightTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index 5b1b24dd9..3aadc3e39 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -364,13 +364,13 @@ public sealed partial class FireControlCut124Tests } } - // Today's sim geometry: the round flies the line frozen at Fire and stops at the arrival point on it, whatever the + // Today's sim geometry (a target crossing at 10 against a speed of 20, so an intercept exists): the round flies the line frozen at Fire and stops at the arrival point on it, whatever the // target does afterwards. (Where a leading round's arrival should lie is the sim's question, never Projectile's.) [Fact] public void AHitOnAMovingTargetStopsOnItsFrozenLine() { var e = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, weaponRange: 1000f); - e.Target.Velocity = float2(30, 0); + e.Target.Velocity = float2(10, 0); FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); var shot = SafeAssert.OnlyShot(e.Zone); Assert.True(shot.TravelDirection.x > 0f, "the round did not lead the target"); From ba2b4a7377a54c4d233c1c69321975145140c931 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:35:46 +0200 Subject: [PATCH 24/49] Projectile draws through DrawAhead and computes no position, time or range Deletes At, the end formula (Unity restating the range) and Resolve's own stop arithmetic. Launch binds ShotCommitted (refresh the record, learn the outcome) and ShotResolved; Draw is DrawAhead.Round plus RoundOver; a hit or burst stops at DrawAhead.RoundStop. Projectile names no flight quantity. Builds headless (Projectile is not in the test project); the Unity compile is run separately at the final head. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Gameplay/Weapons/Projectile.cs | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/Assets/Scripts/Gameplay/Weapons/Projectile.cs b/Assets/Scripts/Gameplay/Weapons/Projectile.cs index 64d84fb4e..2fe4129d1 100644 --- a/Assets/Scripts/Gameplay/Weapons/Projectile.cs +++ b/Assets/Scripts/Gameplay/Weapons/Projectile.cs @@ -2,15 +2,15 @@ using System.Collections; using UniRx; using UnityEngine; -using static CultMath.math; -// A ballistic round, drawn on FireControl.RoundAt and deciding nothing. The simulation owns the round's whole flight -// (origin, direction, speed, fire time, arrival, range: the PendingShot copy taken at spawn), so this object stores no -// velocity or position of its own and integrates nothing: each frame it asks RoundAt where the round is at sim time -// plus the clock's Lead (the same draw-ahead ships use), and the simulation's ShotResolved ends a round that hit or -// burst. A round that missed flies on to the MaxRange point RoundAt clamps at, and is done there. The barrel is only -// where the round is first seen: its offset from the sim line decays to zero over BlendTime sim seconds. The trail's -// fade after the end stays real time (pure presentation, ruling sim-speed-presentation). +// A ballistic round, drawn by DrawAhead's round statics and deciding nothing. The simulation owns the round's whole +// flight (the PendingShot copy taken at spawn, replaced by the sim's record at ShotCommitted, and the outcome it +// publishes), so this object stores no motion state of its own and computes no position, time or range: each frame it +// asks DrawAhead.Round where the round is at sim time plus the clock's Lead (the same draw-ahead ships use), and the +// round ends on the sim's word: ShotResolved stops a round that hit or burst where DrawAhead.RoundStop puts it, and +// DrawAhead.RoundOver ends one the sim published as a miss. The barrel is only where the round is first seen: its +// offset from the sim line decays to zero over BlendTime sim seconds. The trail's fade after the end stays real time +// (pure presentation, ruling sim-speed-presentation). public class Projectile : MonoBehaviour { public TrailRenderer Trail; @@ -21,50 +21,59 @@ public class Projectile : MonoBehaviour private PendingShot _shot; private Zone _zone; - private Vector3 _barrelOffset; + private CultMath.float3 _barrel; + private ShotResult? _known; private bool _alive; - private IDisposable _binding; + private CompositeDisposable _binding; - // Takes the frozen shot, the zone whose sim time it is drawn at, and where the barrel is as this round is first seen. + // Takes the shot as the sim holds it, the zone whose sim time it is drawn at, and where the barrel is as this round is first seen. public void Launch(Zone zone, PendingShot shot, Vector3 barrel) { _zone = zone; _shot = shot; - var origin = shot.FireOrigin; - _barrelOffset = barrel - new Vector3(origin.x, origin.y, origin.z); + _barrel = new CultMath.float3(barrel.x, barrel.y, barrel.z); + _known = null; _binding?.Dispose(); - _binding = zone.ShotResolved.Where(o => o.ShotId == shot.ShotId).Subscribe(Resolve); + _binding = new CompositeDisposable( + zone.ShotCommitted.Where(o => o.ShotId == shot.ShotId).Subscribe(Committed), + zone.ShotResolved.Where(o => o.ShotId == shot.ShotId).Subscribe(Resolve)); _alive = true; transform.forward = new Vector3(shot.TravelDirection.x, 0, shot.TravelDirection.y); - transform.position = barrel; - } - - private Vector3 At(CultMath.float2 planar, float time) - { - var blend = BlendTime > 0 ? saturate(1 - (time - _shot.FireTime) / BlendTime) : 0f; - return new Vector3(planar.x, _shot.FireOrigin.y, planar.y) + _barrelOffset * blend; + Draw(); } void Update() { - if (!_alive) return; - var time = _zone.Time + ActionGameManager.Clock.Lead; - transform.position = At(FireControl.RoundAt(_shot, time), time); - var end = _shot.Speed > .01f ? _shot.FireTime + _shot.MaxRange / _shot.Speed : _shot.FireTime; - if (time >= end) Finish(); + if (_alive) Draw(); } - // The simulation's verdict. A round that hit or burst stops where the sim put it, at the outcome's burst point, - // else where it arrives on its line; a miss keeps flying. + private void Draw() + { + var time = _zone.Time + ActionGameManager.Clock.Lead; + transform.position = V(DrawAhead.Round(_shot, _known, _barrel, BlendTime, time)); + if (DrawAhead.RoundOver(_shot, _known, time)) Finish(); + } + + // The sim's commit: the record it holds now (a contact burst's arrival is shortened here) and what it decided. + private void Committed(ShotOutcome outcome) + { + if (_zone.TryGetShot(outcome.ShotId, out var record)) _shot = record; + _known = outcome.Result; + } + + // The simulation's verdict. A round that hit or burst stops where the sim put it; a miss keeps flying. private void Resolve(ShotOutcome outcome) { - if (!_alive || (outcome.Result != ShotResult.Hit && outcome.Result != ShotResult.Burst)) return; - var planar = outcome.HasBurstPoint ? outcome.BurstPoint : FireControl.RoundAt(_shot, _shot.ArrivalTime); - transform.position = At(planar, _shot.ArrivalTime); + if (!_alive) return; + _known = outcome.Result; + if (outcome.Result == ShotResult.Miss) return; + transform.position = V(DrawAhead.RoundStop(_shot, outcome, _barrel, BlendTime)); if (HitEffect != null) HitEffect.Instantiate().position = transform.position; Finish(); } + private static Vector3 V(CultMath.float3 p) => new Vector3(p.x, p.y, p.z); + private void Finish() { _alive = false; From d98818ea0c1c669995f3326559f7722ce78287a7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 22:56:51 +0200 Subject: [PATCH 25/49] BallisticFlightTests: a receding proximity target puts the burst point off the arrival line The contact-burst fixture cannot tell RoundStop's burst point from the line at arrival (CommitBurst puts both at the same distance), so a stop that ignored the burst point survived. A targeted proximity round at a target receding faster than the round closes bursts at the pulled-back range point while arriving at the fired-at distance; this test pins the stop to the sim's burst point. Co-Authored-By: Claude Opus 5.5 --- .../BallisticFlightTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index 3aadc3e39..f2c5b45f7 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -382,4 +382,31 @@ public sealed partial class FireControlCut124Tests Assert.Equal(shot.FireOrigin.y, stop.y, 3); Assert.Equal(line.y, stop.z, 3); } + + // A proximity round at a target that recedes faster than the round closes bursts at the weapon's range, pulled back + // along its line, while it arrives when it has flown the distance the target was fired at: its burst point is not + // the line at its arrival. The stop is the sim's burst point, so a presenter that stopped at the arrival line would + // end the tracer short of where the round burst. + [Fact] + public void ABurstRoundStopsAtItsBurstPointNotItsArrivalLine() + { + var e = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, fuse: WeaponFuse.Proximity, blastRadius: 4f, + damage: 100f, weaponRange: NoLockRange, targetRange: 50f); + e.Target.Velocity = float2(0, 15); + ShotOutcome outcome = null; + using var c = e.Zone.ShotCommitted.Subscribe(o => outcome = o); + FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + var origin = SafeAssert.OnlyShot(e.Zone).FireOrigin; + for (var i = 0; i < 100 && outcome == null; i++) e.Zone.Update(.05f); + Assert.NotNull(outcome); + Assert.True(outcome.HasBurstPoint); + Assert.True(e.Zone.TryGetShot(outcome.ShotId, out var record)); + + Assert.Equal(origin.x, outcome.BurstPoint.x, 3); + Assert.Equal(origin.z + NoLockRange, outcome.BurstPoint.y, 3); + Assert.Equal(origin.z + 50f, FireControl.RoundAt(record, outcome.Result, record.ArrivalTime).y, 3); + var stop = DrawAhead.RoundStop(record, outcome, record.FireOrigin, 0f); + Assert.Equal(outcome.BurstPoint.x, stop.x, 3); + Assert.Equal(outcome.BurstPoint.y, stop.z, 3); + } } From ce4bb199a7f368921983dd5d492aeeaeeb8de8e0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:23:24 +0200 Subject: [PATCH 26/49] Map: ballistic flight is presentation (r3 probes and rationale) Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index b095dc870..255ed25a5 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -5844,6 +5844,17 @@ factor), which waits on all of them. **Why a round's end is one sim statement.** The sim resolves a round at `ArrivalTime`. A drawn round that ends anywhere else needs a second owner of the end, which is what ballistic-flight s1 found in Projectile. `FireControl.RoundEnd(shot, known)` states the end for what the sim has published about the outcome: arrival for a hit, a burst or an unknown outcome, and the frozen weapon range for a miss, never short of arrival. `RoundAt` clamps there. A round whose outcome is not yet published waits at its arrival point for at most one step instead of overshooting. The drawing rules (placement, barrel blend, stop, done) are pure statics in DrawAhead, beside the ships' draw-ahead, so the headless suite tests them by behaviour rather than by grep. +**Ballistic flight is presentation (r3, 2026-10-09).** After rulings projectile-flight-is-presentation, sim-hooks-presentation-agnostic and direct-arrival-keep-timing, FireControl owns only a shot's facts: the outcome, when damage lands (ArrivalTime, a contact burst's shortened arrival, the commit tick), the impact cell with its target, and a fused round's burst point. DrawAhead owns the drawn round, as pure statics tested headless. + +Probes at d98818ea: +(1) The impact cell is already on the outcome: on a hit, Commit stores the first cell of the shot's lane (FireControl.cs:960-988), MakeOutcome copies Cell and Target (:1527-1544), and ShotCommitted and ShotResolved publish the outcome (:881, :894). No sim hook was needed. +(2) Entity.ToWorldPoint (Entity.cs:717-725) is the one schematic-to-world frame. The step turns Direction by `mul(Direction, float2x2.Rotate(TurnRate*dt))` (Thruster.cs:129), so the cell on the drawn pose is `Position(target, lead).xz + Rotate(ToWorldPoint(cell) - Position.xz, TurnRate*lead)`, exactly. +(3) The commit tick equals `record.ArrivalTime - outcome.ArrivalIn` for every result (:877-881, :1541). +(4) An engaged proximity round is Hit or Burst, never Miss; it bursts at BurstPosition frozen at Fire (:1009-1017, :1148-1150), and it is drawn there. +(5) A target that leaves the zone mid-flight resolves as a fresh Miss (:857-868). + +Rationale: a hit follows its frozen line until the commit tick, then blends onto its impact cell on the target's drawn pose, so it lands on the hull rather than at the frozen line's arrival point. The sim's damage timing is unchanged. A miss flies on to the weapon's frozen range, which is DrawAhead.RoundEnd's choice. A kink at the commit frame (at most one step's share of the line-to-hull gap) and a snap back to the line after a target-gone Miss are accepted presentation. Rejected: publishing the lane's entry point as a new sim field; the ruling names the cell, so a hit effect may sit up to half a cell inside the hull skin. Risk: a hull mesh not aligned with its schematic makes hits land beside the mesh; that is a content fault, and the operator's moving-target check shows it. + ### Census Every Unity-side place that integrates, times or decides a sim fact at `6d427b7a`, and the cut From 29c5f7aba154a24a44c74edd36b8aa13878a64e9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:24:54 +0200 Subject: [PATCH 27/49] Consumable host: one initialiser for gear and running effects; tests pin overlap, lifetime, per-design cooldown, refit Entity.Activate re-initialises the running consumable effects through the same InitializeBehaviors that initialises gear and ActivateConsumable, so a consumable active across deactivate/refit/activate retargets to the new items. ConsumableTests gains the stackable overlap/multiply, shared-entry detach, expiry-boundary, per-design cooldown and refit facts. StatResolverTests' expiry test builds its consumable through ItemManager.CreateInstance. Not yet built or run; verification follows in a later commit message. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Entity.cs | 19 ++-- .../Aetheria.Shared.Tests/ConsumableTests.cs | 99 ++++++++++++++++++- .../StatResolverTests.cs | 2 +- 3 files changed, 109 insertions(+), 11 deletions(-) diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 9c4929058..d7152b172 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -203,12 +203,10 @@ public abstract class Entity return presencePermitted; }), initialValue: true); - foreach (var item in Equipment) - foreach (var behavior in item.Behaviors) - { - if(behavior is IInitializableBehavior initializableBehavior) - initializableBehavior.Initialize(); - } + // One initialiser for everything that computes targets here: gear and the effects still running, so a + // consumable active across a refit retargets to the new items exactly as gear does. + InitializeBehaviors(Equipment.SelectMany(item => item.Behaviors)); + InitializeBehaviors(_activeConsumables.SelectMany(effect => effect.Behaviors)); foreach(var entity in Zone.Entities) { EntityInfoGathered[entity] = 0; @@ -863,10 +861,15 @@ public abstract class Entity var effect = new ConsumableItemEffect(item, this); // The same hook Activate runs over Equipment: a behaviour that computes its targets there (StatModifier) // would otherwise reach its second tick uninitialised. - foreach (var behavior in effect.Behaviors) + InitializeBehaviors(effect.Behaviors); + _activeConsumables.Add(effect); + } + + private static void InitializeBehaviors(IEnumerable behaviors) + { + foreach (var behavior in behaviors.ToList()) if (behavior is IInitializableBehavior initializable) initializable.Initialize(); - _activeConsumables.Add(effect); } public ConsumableItemEffect FindActiveConsumable(ConsumableItemData data) diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs index 740f739ff..843b0f221 100644 --- a/tests/Aetheria.Shared.Tests/ConsumableTests.cs +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -27,10 +27,13 @@ public sealed class ConsumableTests : IDisposable public EquippedItem Engine; public PerformanceStat Thrust; public ConsumableItemData Consumable; + public ConsumableItemData Other; // a second, unrelated non-stackable design + public Func NewEngine; public CultRecordKey Key; public CultRecordRef Maker; - public ConsumableItem Mint() => Assert.IsType(Items.CreateInstance(Items.CreateLot(Consumable, Maker, .5f))); + public ConsumableItem Mint(ConsumableItemData design = null) => + Assert.IsType(Items.CreateInstance(Items.CreateLot(design ?? Consumable, Maker, .5f))); public EquippedCargoBay Hold => Ship.CargoBays.Single(); public int InCargo => Hold.Cargo.Keys.Count(i => i is ConsumableItem); } @@ -70,6 +73,7 @@ public sealed class ConsumableTests : IDisposable } } }); + var otherRef = cache.Upsert(new ConsumableItemData { Name = "Flare", Duration = 1f, Shape = new Shape() }); cache.FlushAsync().Wait(); var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { }); @@ -84,7 +88,8 @@ public sealed class ConsumableTests : IDisposable var data = cache.Get(consumableRef); return new Rig { - Cache = cache, Items = items, Ship = ship, Thrust = thrust, Consumable = data, + Cache = cache, Items = items, Ship = ship, Thrust = thrust, Consumable = data, Other = cache.Get(otherRef), + NewEngine = () => Gear(cache.Get(engineRef)), Key = items.ItemData.RefOf(data).Key, Engine = ship.Equipment.Single(e => e.Data.Name == "Engine"), Maker = makerRef @@ -149,4 +154,94 @@ public sealed class ConsumableTests : IDisposable Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); Assert.Equal(0, rig.InCargo); } + + // The expiry boundary: Duration 1 with 0.3 s ticks leaves 0.1 s after the third tick (still active) and + // -0.2 s after the fourth (gone). An expiry one tick early or one tick late changes one of these two reads. + [Fact] + public void AConsumableLivesForItsDurationAndNoLonger() + { + var rig = Build(); + using var _ = rig.Cache; + rig.Ship.ActivateConsumable(rig.Mint()); + Tick(rig, 3, .3f); + Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); + Tick(rig, 1, .3f); + Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); + } + + // CS-R1, the stackable side: a stackable design activates again while its first effect runs, and every + // multiplier on the stat multiplies. + [Fact] + public void TwoStackableConsumablesOverlapAndMultiply() + { + var rig = Build(stackable: true); + using var _ = rig.Cache; + Assert.True(rig.Hold.TryStore(rig.Mint())); + Assert.True(rig.Hold.TryStore(rig.Mint())); + + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.Equal(0, rig.InCargo); + Tick(rig, 2, .3f); + Assert.Equal(40f, rig.Engine.Evaluate(rig.Thrust), 3); + } + + // Two modifiers share one resolver entry. The first expires while the second still runs: the second's + // boost stays, and when it expires too nothing is left behind. + [Fact] + public void AnExpiringConsumableLeavesAnOverlappingOnesBoost() + { + var rig = Build(stackable: true); + using var _ = rig.Cache; + var resolver = rig.Ship.Resolver; + var baseline = resolver.ModifierEntryCount; + Assert.True(rig.Hold.TryStore(rig.Mint())); + Assert.True(rig.Hold.TryStore(rig.Mint())); + + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Tick(rig, 2, .3f); + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Tick(rig, 2, .3f); // the first ran out on the fourth tick overall; the second has 0.4 s left + Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); + Assert.True(resolver.ModifierEntryCount > baseline); + + Tick(rig, 2, .3f); + Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); + Assert.Equal(baseline, resolver.ModifierEntryCount); + } + + // CS-R1: the cooldown is per design. A running effect of one design does not refuse another design. + [Fact] + public void ADesignsCooldownDoesNotBlockAnotherDesign() + { + var rig = Build(); + using var _ = rig.Cache; + Assert.True(rig.Hold.TryStore(rig.Mint())); + Assert.True(rig.Hold.TryStore(rig.Mint(rig.Other))); + + Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); + Assert.True(rig.Ship.TryActivateConsumable(rig.Other)); + Assert.Equal(0, rig.InCargo); + } + + // Docking: an effect still running when the entity deactivates, is refitted and activates again boosts the + // items now fitted, as gear modifiers do, not the thruster that was swapped out. + [Fact] + public void AConsumableActiveAcrossARefitBoostsTheNewItems() + { + var rig = Build(); + using var _ = rig.Cache; + rig.Ship.ActivateConsumable(rig.Mint()); + Tick(rig, 2, .3f); + Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); + + rig.Ship.Deactivate(); + Assert.NotNull(rig.Ship.TryUnequip(rig.Engine)); + Assert.True(rig.Ship.TryEquip(rig.NewEngine(), EngineCell)); + rig.Ship.Activate(); + Tick(rig, 2, .1f); // 0.2 s more: the effect still has 0.2 s of its second + var fitted = rig.Ship.Equipment.Single(e => e.Data.Name == "Engine"); + Assert.NotSame(rig.Engine, fitted); + Assert.Equal(20f, fitted.Evaluate(rig.Thrust), 3); + } } diff --git a/tests/Aetheria.Shared.Tests/StatResolverTests.cs b/tests/Aetheria.Shared.Tests/StatResolverTests.cs index 9afafcd26..0c0283f20 100644 --- a/tests/Aetheria.Shared.Tests/StatResolverTests.cs +++ b/tests/Aetheria.Shared.Tests/StatResolverTests.cs @@ -441,7 +441,7 @@ public sealed class StatResolverTests : IDisposable for (var i = 0; i < 20; i++) { var lot = items.Lots.Add(new Lot { Design = cache.RefOf(boosterData), Origin = new Attributed(), Quality = .5f, Roles = new List() }); - var consumableItem = new ConsumableItem { Data = cache.RefOf(boosterData), Lot = lot }; + var consumableItem = Assert.IsType(items.CreateInstance(lot)); ship.ActivateConsumable(consumableItem); // Duration is 0.01s; one 1-second tick expires it and drives Entity.Update's expiry branch. From 87ec736f045aabbf0c23ce1e1562e069f4c5c3e8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:25:25 +0200 Subject: [PATCH 28/49] Map: articulated mounts (body facts AM1-AM11, model-page rows, authority map, rationale) Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 188 +++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index 255ed25a5..fe79044d6 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -7328,3 +7328,191 @@ vapour-cloud-presenter r2, modifier-stacking r3 (shrunk to tests), consumables-f follow-up; reshaped by `consumables-scarce`), boost-gear-systems r2 and boost-gear-combat r2 (held), ships-authoring-tool r2, ships-player r3. New: cut player-build r1, follow-up `component-records-after-demo`. No question raised. + +## Articulated mounts + +Mapped 2026-10-09 by Imagination (`imagination-aeth-articulated`, session `self-2026-10-09-ag`) for follow-up +`articulated-mounts`, against `origin/master` `6d83597b`. Rulings: `arc-and-traverse-on-the-link`, +`rig-is-presentation`, `sim-hooks-presentation-agnostic`, `projectile-flight-is-presentation`, +`ship-roles-are-tags`, `thrusters-radiators-are-meshes`; follow-up `articulated-mount-traverse-min`. +Anchors are exact at `6d83597b`. + +**Scope tension for Self.** Target r4's `not_in_scope` lists "articulated mounts". The operator asked for this work +now (her hermit-crab hull carries its big gun on a jointed arm). The cuts are admitted; the target needs a revision +that takes articulated mounts out of `not_in_scope` before Hands is briefed. + +### Body facts + +**AM1. Arc is per hardpoint today, not per weapon.** `FireControl.ArcFor` (`FireControl.cs:36-41`) reads +`HardpointData.FiringArc` (`ItemData.cs:567-569`, key 6) when above zero, else `GameplaySettings.FiringArc` +(`Settings.cs:222`, initializer 120). `InArc` (`:50-58`) passes 360 or more unconditionally. The only shipped writer +was `AetherDb firing-arc-migrate` (`tools/AetherDb/Program.cs:998-1047`), which set 360 on the Turret hull's +Ballistic hardpoints. Twenty-three hardpoint initializers author `FiringArc`, all in tests (`git grep -nE +"[{,] *FiringArc = |^ +FiringArc = "` over tests, Assets/Scripts and tools: 23), and eight accessors read or write +it (`hardpoint\??\.FiringArc|Mount\(s\)\.FiringArc|Hardpoints\[0\]\.FiringArc`: 8). The brief's "per weapon, +default 120, turret 360" is this per-hardpoint rule. + +**AM2. Traverse is not simulated; it only prices.** `Solution` (`FireControl.cs:128-152`) says turrets traverse +instantly in the simulation and that link traverse "will make this stateful behind the same signature". +`TrackingRate` (`:238-245`) is the gun's `Weapon.Tracking` times the ship's targeting gear over `UnaidedTracking`, ++infinity for an unauthored gun, and only `PMount` reads it (`:383`, `:427`, `:720`). Follow-up +`articulated-mount-traverse-min` puts the link's traverse inside `TrackingRate` as the slower of the two. These cuts +add no slew state: arc and aim stay derived on every read. + +**AM3. The visual record reserves a pivot role nobody uses.** `ShipAuthoringStore.Roles` (`ShipAuthoring.cs:50-58`) +includes `articulation`, "a pivot, never a mount". No add-on code tags it, and `ShipModShips.Assemble` gives every +mod ship `ArticulationPoints = Array.Empty()` (`ShipModShips.cs:164`). The authoring doc says no +pivot is planned "until pivots move onto `HullData`" (`docs/moddable-ship-authoring.md:188-189`). + +**AM4. `ArticulationPoint` is one yaw/pitch pivot with no yaw clamp.** `ArticulationPoint.cs:9-48` slews yaw and +pitch toward `Target` at `Speed` and clamps only pitch. `ShipPrefabAuthoring.cs:139-149` parses +`Pivot.Group.YawMin.YawMax.PitchMin.PitchMax.Speed` but stores no yaw limit, so the Controls section's C9 line +("clamps yaw to YawMin..YawMax") is wrong on this base. A single pivot needs no IK. `EntityInstance.cs:282-289` +gives each articulation group an aim transform, and `LateUpdate` places it (`:410-417`) from +`FireControl.Solution(...).Direction` of the group's first weapon. A link presenter follows that read. The Eyes IK +report (AM11) found the same two facts. + +**AM5. The GLB path maps every node that carries `aetheria.id`.** `ShipModCatalog.ReadNodeIds` +(`ShipModCatalog.cs:195-218`) reads node extras, refuses a duplicate id and records whether the node has a mesh; it +keeps no parent relation. `Bind` (`:168-191`) checks each anchor's node. `ShipModVisual.LoadAsync` maps anchors to +Transforms through glTFast's instantiator (`ShipModVisual.cs:51-54`). A joint is an ordinary glTF node, so the same +map reaches it once it carries an id. + +**AM6. Probe: the add-on's export keeps an armature, its skin and data-bone extras.** Run on Starfire 2026-10-09 +with Blender 5.2.2 LTS at `C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe` +(`--background --factory-startup`). Script `probe_rig_export.py` in the session scratchpad +(`...\scratchpad\imagination-aeth-articulated\`). It builds a collection with a hull cube; an armature +Shoulder > Elbow > Wrist; a cylinder skinned to Wrist through an Armature modifier; an empty `weapon-mount` +parented to the Wrist bone (`parent_type = BONE`) with a muzzle child; a Limit Rotation constraint on Elbow; and +custom properties on the Wrist data bone (`aetheria.role=link`, `aetheria.id=link.claw`) and on the Wrist pose bone. +It exports with the add-on's options (`__init__.py:359-361`: GLB, `use_active_collection`, `use_selection`, +`export_extras`, `export_yup`, `export_apply`). Result: +- One skin with joints `Shoulder, Elbow, Wrist`; the barrel node has `skin: 0` and a mesh. `export_apply` did not + bake the armature away. +- Joints are ordinary nodes in the hierarchy (`Arm > Shoulder > Elbow > Wrist > ClawGun > ClawGun.Muzzle`): the + bone-parented empty is a child of the Wrist joint node and keeps its extras. +- Data-bone custom properties export as the joint node's `extras` (`Wrist` carried `aetheria.role` and + `aetheria.id`). The pose-bone property did **not** export. A tag set by hand in Pose Mode's custom-properties + panel is lost; the add-on must write data-bone properties. +- The Limit Rotation constraint left no trace in the GLB (no extension, no extras). glTF carries no joint limits, + so the add-on must package them. +- The armature object's own custom property exported as extras on the `Arm` node. + +**AM7. The ship file carries unknown slots through.** `ship_cc.replace_layout` (`ship_cc.py:161-195`) and +`replace_visual` (`:116-140`) append each row's slots past the known members by mount or anchor id, and edit only +their own slots. `ShipSchemaPinTests` pins the Python slot numbers and member orders to the C# keys and asserts +`HARDPOINT_MEMBERS` has 7 names. A C# cut that adds `HardpointData` key 7 or `HullData` key 33 therefore needs no +Python change: the add-on carries both untouched until the add-on cut names them. + +**AM8. The schema version string is not a gate.** CultCache resolves a persisted record by schema name when the +local type has one version (CultLib `src/GameCult.Caching/CultCache.cs:518-523`). `aetheria.hulldata` and +`aetheria.ship_authoring` are both `"1"` (`ItemData.cs:510`, `ShipAuthoring.cs:12`). Bumping either buys no +refusal. A retired slot is guarded by a raw-payload refusal instead, as `RefuseLegacyEmbeddedHull` +(`ShipAuthoring.cs:124-151`) guards ShipAuthoring key 1. Every cut here keeps both versions at `"1"`. + +**AM9. Calibre is a footprint band, held only in a test so far.** `WeaponItemData.WeaponCaliber` +(`ItemData.cs:482-483`) runs Small to ExtraLarge (`Enums.cs:77-83`). Cut `caliber-bands` (unlanded) states the bands +(Small 1-2 cells, Medium 3-4, Large 5-6, ExtraLarge 7-9) only inside its test. No balance source names an arc cap +per calibre: AetheriaLore `Game Design/Ship Play Concepts.md` fork 2 (line 24, read in the ship-language +worktree) gives the trade without numbers. + +**AM10. In-flight add-on specs that touch arcs.** Among in-force specs with no report, only `ships-addon-gizmos` r3 +(arc fan from the row's arc) and `ships-addon-mounts` r5 (row defaults include `FiringArc`; a negative grep on +`.firing_arc =`) name a hardpoint arc. `controls-hud` r2, `controls-ai-bearing` r1 and `munition-shots` r5 read +`ArcFor` or `TrackingRate`, whose signatures these cuts keep. + +**AM11. Eyes prior art on IK** (`C:\Users\Meta\AppData\Local\Temp\claude\F--Projects-CultLib\f24b705c-99b0-4915-8cd0-d31d652edbbf\scratchpad\eyes-ik-prior-art\ik-prior-art.md`). +Damped least squares, min `||[J; lambda I] dtheta - [e; 0]||^2` with the joint bounds as the box, has exactly the +form of CultMath's existing `BoundedLeastSquares`. Every engine Eyes checked (Unity Animation Rigging, Godot 4.6, +Unreal Control Rig) keeps IK in the animation layer; none was found in a math library. Aim-an-axis leaves twist +about the aim free, so a chain aims with two effective degrees of freedom. + +### Model page + +| Kind | Identity | Lifecycle | Authority | +|---|---|---|---| +| Link (`LinkData` in `HullData.Links`, key 33) | `Id`: the `aetheria.id` that Tag Link writes on the data bone or object (ruling `ship-roles-are-tags`). Renaming never changes it. For a shipped hull, the id the migration writes. | Edited in Blender's layout buffer and written by Save or Package. A package edited after a save changes the arc or traverse a run's guns use on Continue. No save names a link, because the sim holds no slew state (AM2). If a link is removed while a hardpoint names it, the judge fails the package, compose excludes it, and Continue refuses the run, naming the missing design (`RunSave.RequireDesigns`). It adds nothing to `mod-hull-changed-under-run`, because a link change moves no cell. | The author, through the add-on, for arc and traverse. `HullLinks.Validate` judges it. `FireControl` reads it, only through `ArcFor` and `TrackingRate`. | +| Hardpoint on a link (`HardpointData.Link`, key 7) | The hardpoint's mount id (`Transform`) plus the link id. Empty means a fixed mount: the default arc and the gun's own traverse. | Derived at Package from the mount object's ancestry (the nearest ancestor tagged link, through a bone parent), as mounted `Position` is derived from the object. A mount re-parented in Blender changes link at the next Package. For `ambiguous-hardpoint-origin`, a link is one more way two hardpoints at one origin differ (arc, as rotation already does), so it neither fixes nor worsens that defect. The fix there is to save the hardpoint's identity: C4's element id on `HardpointData`. | The mount's place in the Blender hierarchy; Package derives it. The panel and Studio show it read-only for a mounted row. The judge refuses a name that is not a link of the hull. | +| Rig chain (`ShipJoint` in `ShipAuthoring.Joints`, key 5, and the `link` anchors) | Each joint's `Id` is its bone's `aetheria.id`, assigned once by Package (`joint.`) and kept on the data bone. A link anchor's id is its link's id. | Re-rigging changes only the visual record, and every Package and compose judges it again. No save, sim fact or catalog record names a joint. The Unity rig components are cache-only and rebuilt each boot with the prototype. | The author's Blender bone IK settings (lock, limit, min, max), copied by Package. The judge checks the tree and that the yaw reach covers the link's arc. Unity reads and drives it. No joint feeds a sim fact (ruling `rig-is-presentation`). | + +**C4 (follow-up `c4-ship-element-ids`).** Links are a fourth element kind for variants: `LinkData` rows keyed by +`Id`, as hardpoints are keyed by `Transform`. A heavy variant that narrows a link's arc is a tail-slot patch by link +id. `ship_cc` carries link rows' unknown slots by id, as it does for hardpoints and anchors; `ships-addon-links` +lands that. + +### Authority map + +- **Owner.** `LinkData`, authored per hull, owns arc width and traverse speed. The Blender hierarchy owns whether a + hardpoint is on a link, derived into `HardpointData.Link` at Package. The Blender bone IK settings own the joint + limits, packaged into `ShipAuthoring.Joints`. +- **Sim inputs.** `HullData.Links` and `HardpointData.Link`, nothing else. +- **Outputs.** `ArcFor`, `InArc`, `AimDirection` and `TrackingRate`, signatures unchanged. Presentation reads + `GunSolution.Direction` of a link's lead weapon and the link's traverse. +- **Derived.** `HardpointData.Link`, from the hierarchy. A fixed mount's arc, which is `GameplaySettings.FiringArc`. + An object link's joint (one yaw degree of freedom, plus or minus half the arc), at Package. The Unity link rigs + (cache-only). +- **Demoted, then cut.** `HardpointData.FiringArc` is no longer an owner; the link is. `links-record` refuses a + hardpoint that names a link while its `FiringArc` is set. `links-arc` stops reading it and refuses it on packages. + `retire-firing-arc` deletes it and retires key 6. `link-rig-record` replaces the `articulation` role with `link`. +- **Forbidden writers.** No joint limit, bone length, IK result or Unity transform feeds `ArcFor` or + `TrackingRate`. No panel or Studio edit writes a mounted row's `Link`. No helper invents an arc or a traverse. + +### Rationale + +**Links are flat in `HullData`; chains live in the visual record.** The sim reads only the link a hardpoint names +(ruling `rig-is-presentation`). A turret on a turret is a chain of joints, and a chain is presentation, so `LinkData` +has no parent: its arc is the whole coverage the author grants that part. Parenting lives in `ShipJoint.Parent`. A +link's chain runs from its joint up to the nearest ancestor that is a link, or to the root, and parent links solve +first. + +**One facing per link.** Weapon hardpoints on one link must share a `Rotation`. `MountDirection` stays derived from +the item's rotation (`FireControl.cs:28-33`), the link needs no facing field, and the presenter's lead weapon aims +for every gun on the part. + +**Why `FiringArc` is retired rather than kept for fixed mounts.** Two places that state an arc are two owners. A +fixed mount is the degenerate case: no link, the default arc, the gun's own traverse. Any other arc is a link, +including the shipped Turret hull's, which migrates to one link with arc 360 and traverse +infinity, so its behaviour +is unchanged. The subtraction is its own cut and lands after the add-on cuts that edit the same panel, so no open +cut's hunks are stranded. + +**Traverse +infinity means no mechanical limit.** It matches `Weapon.Tracking`'s convention ("an unauthored gun is ++infinity", `FireControl.cs:238-245`), so `min` needs no special case. The add-on shows it as a Free traverse +toggle; zero and NaN are refused. + +**Joint limits come from Blender's bone IK settings, not from Limit Rotation constraints.** Blender's own IK solver +honours `lock_ik_*`, `use_ik_limit_*` and `ik_min_*`/`ik_max_*`, so an author previewing with an IK constraint sees +the limits the game uses. The semantics follow Blender's: an axis is free unless locked or limited. Package writes +each degree of freedom as a unit axis in Ship Root space at rest, converted to glTF axes, with a min and max in +degrees. This keeps the record independent of the exporter's bone-axis correction, which this map did not verify. + +**The reach check is yaw only, and conservative.** The sim is planar. A link's chain covers its arc when the summed +ranges of the chain's degrees of freedom whose axis lies within 5 degrees of Ship Root up reach the arc (an arc of +360 needs a summed 360). Serial joints about parallel axes add, so the sum is exact for the arms the add-on makes. A +chain whose yaw comes only from tilted axes is refused with a message naming the link. That is the price of a check +the judge can state in one line. + +**The solver is behind a port.** Aetheria's presenter needs only this: a chain (joints root-first, their rest +offsets, their degrees of freedom with limits), an effector (the muzzle offset and its local aim axis), the current +angles and a target direction; it gets back new angles. `link-rig-presenter` codes against that port. Question +`solver-owner` decides who implements it. + +**Thrusters on links stay a follow-up.** A gimballed thruster changes where thrust points. That is a sim fact, and +the thrust allocator (cuts `thrust-allocator-core`, `thrust-allocator` and `envelope-from-columns`, all in flight) +reads thrusters as fixed columns. Putting thrusters on links now would reopen those cuts. The judge refuses a +non-weapon hardpoint on a link and names follow-up `gimballed-thrusters`. + +**Cut order.** `links-record` (types and judge), then `links-arc` (FireControl reads the link; the Turret hull and +the test fixtures migrate), carry the sim. `link-rig-record` adds the joint record and the GLB checks. +`ships-addon-links` lands the Blender side after `ships-addon-mounts`. Mounts r5 needs no revision: it writes no +arc, and `ships-addon-links` extends its derivation. `ships-addon-gizmos` r4 replaces r3. `link-rig-presenter` +waits on `solver-owner`, `link-calibre-judge` waits on `calibre-arc-caps`, and `retire-firing-arc` goes last. + +### Admitted + +Receipts `mind-commit-b0c6b373...`, `mind-commit-c00a16b5...`, `mind-commit-08fce00f...` and `mind-commit-06e09a45...` +(2026-10-09 21:18-21:24 UTC). New cuts: links-record r1, links-arc r1, link-rig-record r2 (r1 superseded for one +out-of-range anchor), ships-addon-link-rows r1, ships-addon-link-package r1, link-rig-presenter r1, link-calibre-judge +r1, retire-firing-arc r1. Revised: ships-addon-gizmos r5 (r3 superseded by r4 and r4 by r5 for an untested negative +pattern). ships-addon-mounts r5 is unchanged. Questions: `solver-owner` (raised in link-rig-presenter) and +`calibre-arc-caps` (raised in link-calibre-judge). Follow-ups: `gimballed-thrusters`, `caliber-bands-one-table`. +context-pack read every anchor SAME at `6d83597b`. From 2dcb7b2078af66c9666bcabc2ca86e8e4955913b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:27:22 +0200 Subject: [PATCH 29/49] Ballistic flight r3: DrawAhead draws a round from the sim's facts, a hit ends at its impact cell; FireControl draws nothing FireControl.RoundEnd and RoundAt leave FireControl (RoundEnd to DrawAhead verbatim, RoundAt as DrawAhead's private Line); DrawAhead.Round takes the published outcome, steers a hit or burst from its frozen line at the commit tick onto its end (burst point, else the impact cell on the target's drawn pose), and RoundStop is deleted. Projectile caches the last published ShotOutcome and calls DrawAhead only. BallisticFlightTests move to the new signatures; the frozen-line moving-target test is replaced by impact-cell, steer and no-drawing-function tests. Unverified: not yet built or run. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Gameplay/Weapons/Projectile.cs | 28 +- .../Gameplay/Weapons/ProjectileManager.cs | 2 +- Assets/Scripts/ServerShared/DrawAhead.cs | 67 +++- Assets/Scripts/ServerShared/FireControl.cs | 24 +- .../BallisticFlightTests.cs | 309 ++++++++++++------ 5 files changed, 286 insertions(+), 144 deletions(-) diff --git a/Assets/Scripts/Gameplay/Weapons/Projectile.cs b/Assets/Scripts/Gameplay/Weapons/Projectile.cs index 2fe4129d1..66a444d89 100644 --- a/Assets/Scripts/Gameplay/Weapons/Projectile.cs +++ b/Assets/Scripts/Gameplay/Weapons/Projectile.cs @@ -3,13 +3,13 @@ using System.Collections; using UniRx; using UnityEngine; -// A ballistic round, drawn by DrawAhead's round statics and deciding nothing. The simulation owns the round's whole -// flight (the PendingShot copy taken at spawn, replaced by the sim's record at ShotCommitted, and the outcome it -// publishes), so this object stores no motion state of its own and computes no position, time or range: each frame it -// asks DrawAhead.Round where the round is at sim time plus the clock's Lead (the same draw-ahead ships use), and the -// round ends on the sim's word: ShotResolved stops a round that hit or burst where DrawAhead.RoundStop puts it, and +// A ballistic round, drawn by DrawAhead.Round from what the simulation publishes and deciding nothing. The simulation +// owns the round's facts (the PendingShot copy taken at spawn, replaced by the sim's record at ShotCommitted, and the +// outcome it publishes), so this object stores no motion state of its own and computes no position, time, range or +// impact point: each frame it asks DrawAhead.Round where the round is at sim time plus the clock's Lead (the same +// draw-ahead ships use). The round ends on the sim's word: ShotResolved ends one that hit or burst, and // DrawAhead.RoundOver ends one the sim published as a miss. The barrel is only where the round is first seen: its -// offset from the sim line decays to zero over BlendTime sim seconds. The trail's fade after the end stays real time +// offset from the drawn line decays to zero over BlendTime sim seconds. The trail's fade after the end stays real time // (pure presentation, ruling sim-speed-presentation). public class Projectile : MonoBehaviour { @@ -22,7 +22,7 @@ public class Projectile : MonoBehaviour private PendingShot _shot; private Zone _zone; private CultMath.float3 _barrel; - private ShotResult? _known; + private ShotOutcome _known; private bool _alive; private CompositeDisposable _binding; @@ -49,25 +49,25 @@ public class Projectile : MonoBehaviour private void Draw() { - var time = _zone.Time + ActionGameManager.Clock.Lead; - transform.position = V(DrawAhead.Round(_shot, _known, _barrel, BlendTime, time)); - if (DrawAhead.RoundOver(_shot, _known, time)) Finish(); + var lead = ActionGameManager.Clock.Lead; + transform.position = V(DrawAhead.Round(_shot, _known, _barrel, BlendTime, _zone.Time, lead)); + if (DrawAhead.RoundOver(_shot, _known, _zone.Time, lead)) Finish(); } // The sim's commit: the record it holds now (a contact burst's arrival is shortened here) and what it decided. private void Committed(ShotOutcome outcome) { if (_zone.TryGetShot(outcome.ShotId, out var record)) _shot = record; - _known = outcome.Result; + _known = outcome; } - // The simulation's verdict. A round that hit or burst stops where the sim put it; a miss keeps flying. + // The simulation's verdict. A round that hit or burst is drawn at its end and stops; a miss keeps flying. private void Resolve(ShotOutcome outcome) { if (!_alive) return; - _known = outcome.Result; + _known = outcome; if (outcome.Result == ShotResult.Miss) return; - transform.position = V(DrawAhead.RoundStop(_shot, outcome, _barrel, BlendTime)); + Draw(); if (HitEffect != null) HitEffect.Instantiate().position = transform.position; Finish(); } diff --git a/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs b/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs index 83eba9ea4..216756cf9 100644 --- a/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs +++ b/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs @@ -4,7 +4,7 @@ public class ProjectileManager : InstantWeaponEffectManager { public Prototype ProjectilePrototype; - // The round is drawn on the line the simulation flies it (FireControl.RoundAt), so there is nothing to aim, jitter + // The round is drawn by DrawAhead from what the simulation publishes, so there is nothing to aim, jitter // or inherit here: spread is priced by FireControl, and the sim's round inherits nothing from its shooter. public override void Fire(InstantWeapon weapon, EquippedItem item, EntityInstance source, EntityInstance target, int shotId) { diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index f594b33fe..98b70656e 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -5,10 +5,11 @@ using CultMath; using static CultMath.math; -// Where a presenter draws the latest sim state when the frame falls Lead sim seconds after the last step: ships by -// their last step's velocity, ballistic rounds on FireControl's RoundAt and RoundEnd. Pure reads of the simulation for -// presenters (never called by ServerShared, no state of their own): the sim owns every pose and no presenter stores -// one, a previous state or a velocity of its own. +// Pure presentation statics over sim state, never called by ServerShared, with no state of their own: the sim owns every +// pose and no presenter stores one, a previous state or a velocity of its own. Ships are drawn where the frame falls Lead +// sim seconds after the last step, by their last step's velocity and turn. A ballistic round's flight is presentation +// (ruling projectile-flight-is-presentation), drawn from the sim's facts: its frozen line, the published outcome, the +// commit tick, the impact cell on the target's drawn pose, the burst point. public static class DrawAhead { // A ship in a wormhole animation has its pose scripted by the step, not integrated from Velocity and TurnRate, @@ -39,19 +40,57 @@ public static class DrawAhead t.w * q.w - t.x * q.x - t.y * q.y - t.z * q.z)); } - // A ballistic round at a sim time (the clock's Zone.Time + Lead): on FireControl's line for what the sim has published - // of its outcome, with the barrel's offset from that line decaying to nothing over blendTime sim seconds from the moment of firing. - public static float3 Round(in PendingShot shot, ShotResult? known, float3 barrel, float blendTime, float time) => - Lifted(shot, FireControl.RoundAt(shot, known, time), barrel, blendTime, time); + // A ballistic round at a sim time (the clock's Zone.Time + Lead): on its frozen line while the outcome is unknown or a + // miss, with the barrel's offset from that line decaying to nothing over blendTime sim seconds from the moment of firing. + // A round the sim published as a hit or burst steers from its line, from the commit tick, onto where it ends by the + // time damage lands: a burst's frozen point, else the impact cell on the target's drawn pose; and is held there after. + public static float3 Round(in PendingShot shot, ShotOutcome known, float3 barrel, float blendTime, float simTime, float lead) + { + var time = simTime + lead; + var drawn = Lifted(shot, Line(shot, known?.Result, time), barrel, blendTime, time); + if (known == null || known.Result == ShotResult.Miss) return drawn; + var end = known.HasBurstPoint + ? float3(known.BurstPoint.x, shot.FireOrigin.y, known.BurstPoint.y) + : Impact(known.Target, known.Cell, lead); + var commit = shot.ArrivalTime - known.ArrivalIn; + var steer = shot.ArrivalTime > commit ? saturate((time - commit) / (shot.ArrivalTime - commit)) : 1f; + return drawn + (end - drawn) * steer; + } - // Where a round that hit or burst stops: the outcome's burst point, else where it arrives on its line. - public static float3 RoundStop(in PendingShot shot, ShotOutcome outcome, float3 barrel, float blendTime) => - Lifted(shot, outcome.HasBurstPoint ? outcome.BurstPoint : FireControl.RoundAt(shot, outcome.Result, shot.ArrivalTime), - barrel, blendTime, shot.ArrivalTime); + // The impact cell's centre on the target's drawn pose: the cell at the sim pose, drawn ahead by the target's last + // step velocity and turned by its last step turn for lead seconds, as Position and Rotation draw the entity. + public static float3 Impact(Entity target, int2 cell, float lead) + { + var at = Position(target, lead); + var offset = target.ToWorldPoint((float2) cell) - target.Position.xz; + var turned = mul(offset, float2x2.Rotate(Turn(target, lead))); + return float3(at.x + turned.x, at.y, at.z + turned.y); + } // Whether the drawn flight is over by time: only a published miss ends by the clock. A hit or burst ends on ShotResolved. - public static bool RoundOver(in PendingShot shot, ShotResult? known, float time) => - known == ShotResult.Miss && time >= FireControl.RoundEnd(shot, ShotResult.Miss); + public static bool RoundOver(in PendingShot shot, ShotOutcome known, float simTime, float lead) => + known != null && known.Result == ShotResult.Miss && simTime + lead >= RoundEnd(shot, ShotResult.Miss); + + // The sim time a round's drawn line ends, for what is known of its outcome. A round that hit or burst (or whose outcome + // is not yet published) ends at its ArrivalTime; a round the sim has published as a Miss flies on to the weapon's frozen + // range and never ends short of its own arrival (a direct round's FireRange is measured to any target, in range or not). + public static float RoundEnd(in PendingShot shot, ShotResult? known) => + known == ShotResult.Miss + ? max(shot.ArrivalTime, shot.FireTime + (shot.Speed > .01f ? shot.MaxRange / shot.Speed : 0f)) + : shot.ArrivalTime; + + // The frozen line a round follows until it steers: from its frozen origin along its frozen direction at its frozen + // speed, held at RoundEnd once it has flown that far. With no outcome known the round is drawn no further than its + // arrival point and waits there for the sim to publish one. + private static float2 Line(in PendingShot shot, ShotResult? known, float time) + { + if (shot.Speed <= .01f) return shot.FireOrigin.xz; + var flown = clamp(time - shot.FireTime, 0f, RoundEnd(shot, known) - shot.FireTime); + return shot.FireOrigin.xz + shot.TravelDirection * (shot.Speed * flown); + } + + private static float Turn(Entity entity, float lead) => + entity is Ship { WormholeAnimationInProgress: true } ? 0f : entity.TurnRate * lead; private static float3 Lifted(in PendingShot shot, float2 planar, float3 barrel, float blendTime, float time) { diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index c1c4e8ddd..fbe2e0d13 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -514,26 +514,6 @@ public static class FireControl return shot.Target == null ? 1f : PDeviation(deviation, shot.Tracking); } - // The sim time a ballistic round's drawn flight ends, for what is known of its outcome: the one statement of a round's - // end. A round that hit or burst (or whose outcome is not yet published) ends at its ArrivalTime. A round the sim has - // published as a Miss flies on to the weapon's frozen range and never ends short of its own arrival (a direct round's - // FireRange is measured to any target, in range or not). Pure: it reads the shot alone, and the simulation's own - // arithmetic (Step, Commit, Apply) does not call it. Presenters end rounds here, through DrawAhead. - public static float RoundEnd(in PendingShot shot, ShotResult? known) => - known == ShotResult.Miss - ? max(shot.ArrivalTime, shot.FireTime + (shot.Speed > .01f ? shot.MaxRange / shot.Speed : 0f)) - : shot.ArrivalTime; - - // Where a ballistic round is at a sim time: the one statement of the round's line, from its frozen origin along its - // frozen direction at its frozen speed, held at RoundEnd once it has flown that far. With no outcome known the round is - // drawn no further than its arrival point and waits there for the sim to publish one. Pure, like RoundEnd. - public static float2 RoundAt(in PendingShot shot, ShotResult? known, float time) - { - if (shot.Speed <= .01f) return shot.FireOrigin.xz; - var flown = clamp(time - shot.FireTime, 0f, RoundEnd(shot, known) - shot.FireTime); - return shot.FireOrigin.xz + shot.TravelDirection * (shot.Speed * flown); - } - // How far a target can and does move off the line of fire in the solution window: the term DeviationProbability // and HitProbability both add. Behaviour (its observed unpredictability across the line of sight) realises the // evasion and capability (its envelope's reach) caps it, so a coasting ship evades nothing whatever it could do @@ -2023,9 +2003,9 @@ public struct PendingShot public float3 FireTargetPosition; public float3 FireTargetVelocity; public float FireTime; - // The weapon's Velocity at Fire: frozen with the rest, so RoundAt states the same line whatever happens to the stat later. + // The weapon's Velocity at Fire, frozen with the rest so a presenter draws the same line whatever happens to the stat later. public float Speed; - // The weapon's Range at Fire, likewise frozen: how far a round that misses flies on, read only by RoundEnd. FireRange above + // The weapon's Range at Fire, likewise frozen: how far a presenter may draw a round that misses. FireRange above // is the distance to the engaged target (0 for a round with none), not how far a round that misses flies on. public float MaxRange; public float CommitTime; diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index f2c5b45f7..61a733e3a 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -4,18 +4,23 @@ using System; using System.Linq; +using System.Reflection; using UniRx; using Xunit; using static CultMath.math; using float2 = CultMath.float2; +using float2x2 = CultMath.float2x2; using float3 = CultMath.float3; +using int2 = CultMath.int2; -// Ballistic flight: FireControl.RoundAt is the one statement of where a fired round is at a sim time, from the -// PendingShot's frozen origin, direction, speed and fire time, and FireControl.RoundEnd the one statement of when its -// drawn flight ends for what the sim has published of its outcome. Unity draws rounds through DrawAhead's round statics -// and integrates nothing, so these tests pin the line, the end and the drawing against first principles (the shooter's -// position, the aim, the authored velocity and range) rather than against the code that computes them. Shares the -// Cut 12.4 engagement fixture. +// Ballistic flight: a round's flight is presentation (ruling projectile-flight-is-presentation). The simulation publishes +// the facts (the PendingShot's frozen origin, direction, speed, fire time and arrival, the outcome with its commit tick, +// impact cell and burst point) and DrawAhead's statics draw the round from them: Round the one statement of where a round +// is at a sim time, Impact where a hit lands on the target's drawn pose, RoundEnd and RoundOver when its drawn flight +// ends. FireControl owns none of it (ruling direct-arrival-keep-timing: damage keeps landing at the arrival time, the +// hit is drawn at its cell). Unity's Projectile calls Round and integrates nothing, so these tests pin the drawing +// against first principles (the shooter's position, the aim, the authored velocity and range, the hull's own frame) +// rather than against the code that computes it. Shares the Cut 12.4 engagement fixture. public sealed partial class FireControlCut124Tests { private const float BallisticSpeed = 20f; @@ -32,57 +37,77 @@ public sealed partial class FireControlCut124Tests return e; } + // What the sim publishes for a shot, as a presenter holds it: only the result (no impact, no burst point). + private static ShotOutcome Known(in PendingShot shot, ShotResult result) => + new ShotOutcome { ShotId = shot.ShotId, Result = result, ArrivalIn = max(0f, shot.ArrivalTime - shot.CommitTime) }; + + // A hit on a cell of the target, committed at `commit`: ArrivalIn is how long after the commit damage lands. + private static ShotOutcome Hit(in PendingShot shot, Entity target, int2 cell, float commit) => + new ShotOutcome { ShotId = shot.ShotId, Result = ShotResult.Hit, Target = target, Cell = cell, ArrivalIn = shot.ArrivalTime - commit }; + + // The hull's occupied coordinate farthest from its centre of mass: a cell well off the target's position. + private static int2 FarCell(Engagement e) => + e.HullData.Shape.Coordinates.OrderByDescending(c => length((float2) c - e.HullData.Shape.CenterOfMass)).First(); + [Fact] - public void RoundAtIsTheFiredLine() + public void ARoundIsDrawnOnItsFiredLine() { // Burst round: leaves the shooter, is where its own speed puts it a second in, and ends on the burst point. var e = FireBurstRound(out var origin); - var burst = SafeAssert.OnlyShot(e.Zone); + ShotOutcome outcome = null; + PendingShot burst = default; + using var c = e.Zone.ShotCommitted.Subscribe(o => + { + outcome = o; + e.Zone.TryGetShot(o.ShotId, out burst); + }); + e.Zone.Update(.01f); + Assert.NotNull(outcome); + Assert.True(outcome.HasBurstPoint); var aim = normalize(float2(2, 1)); // The shooter moves on after the round left: the line is the one frozen at Fire, not one drawn from where it is now. e.Shooter.Position += float3(7, 0, 3); - var start = FireControl.RoundAt(burst, null, burst.FireTime); + var start = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime, 0f); Assert.Equal(origin.x, start.x, 3); - Assert.Equal(origin.z, start.y, 3); + Assert.Equal(origin.z, start.z, 3); - var second = FireControl.RoundAt(burst, null, burst.FireTime + 1f); + var second = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime + 1f, 0f); Assert.Equal(origin.x + aim.x * BallisticSpeed, second.x, 3); - Assert.Equal(origin.z + aim.y * BallisticSpeed, second.y, 3); + Assert.Equal(origin.z + aim.y * BallisticSpeed, second.z, 3); - foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Burst }) + foreach (var known in new[] { null, outcome }) foreach (var later in new[] { 0f, 1f, 50f }) { - var arrival = FireControl.RoundAt(burst, known, burst.ArrivalTime + later); + var arrival = DrawAhead.Round(burst, known, burst.FireOrigin, 0f, burst.ArrivalTime + later, 0f); Assert.Equal(burst.BurstPosition.x, arrival.x, 3); - Assert.Equal(burst.BurstPosition.z, arrival.y, 3); + Assert.Equal(burst.BurstPosition.z, arrival.z, 3); } // Past the weapon's range it is held at the range point, however late it is asked. - var held = FireControl.RoundAt(burst, ShotResult.Miss, burst.FireTime + NoLockRange / BallisticSpeed + 50f); + var held = DrawAhead.Round(burst, Known(burst, ShotResult.Miss), burst.FireOrigin, 0f, burst.FireTime + NoLockRange / BallisticSpeed + 50f, 0f); Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); - Assert.Equal(origin.z + aim.y * NoLockRange, held.y, 3); + Assert.Equal(origin.z + aim.y * NoLockRange, held.z, 3); // Before it was fired it is still at the origin, not behind it. - var before = FireControl.RoundAt(burst, null, burst.FireTime - 5f); + var before = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime - 5f, 0f); Assert.Equal(origin.x, before.x, 3); - Assert.Equal(origin.z, before.y, 3); + Assert.Equal(origin.z, before.z, 3); // Direct round at a stationary target 100 ahead (+z): arrives on the target, and a miss would fly on to the weapon's range. var d = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, weaponRange: 1000f); var dOrigin = d.Shooter.Position; FireControl.Fire(d.Weapon, d.WeaponItem, d.Shooter); var direct = SafeAssert.OnlyShot(d.Zone); - foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Hit }) - // A round not known to miss waits at its arrival point however late it is asked. - foreach (var at in new[] { direct.ArrivalTime, direct.FireTime + 99f }) - { - var hit = FireControl.RoundAt(direct, known, at); - Assert.Equal(d.Target.Position.x, hit.x, 2); - Assert.Equal(d.Target.Position.z, hit.y, 2); - } - Assert.Equal(dOrigin.z + 100f / 2f, FireControl.RoundAt(direct, null, direct.FireTime + 2.5f).y, 2); - var heldDirect = FireControl.RoundAt(direct, ShotResult.Miss, direct.FireTime + 1000f / BallisticSpeed + 99f); - Assert.Equal(dOrigin.z + 1000f, heldDirect.y, 2); + // A round not known to miss waits at its arrival point however late it is asked. + foreach (var at in new[] { direct.ArrivalTime, direct.FireTime + 99f }) + { + var waiting = DrawAhead.Round(direct, null, direct.FireOrigin, 0f, at, 0f); + Assert.Equal(d.Target.Position.x, waiting.x, 2); + Assert.Equal(d.Target.Position.z, waiting.z, 2); + } + Assert.Equal(dOrigin.z + 100f / 2f, DrawAhead.Round(direct, null, direct.FireOrigin, 0f, direct.FireTime + 2.5f, 0f).z, 2); + var heldDirect = DrawAhead.Round(direct, Known(direct, ShotResult.Miss), direct.FireOrigin, 0f, direct.FireTime + 1000f / BallisticSpeed + 99f, 0f); + Assert.Equal(dOrigin.z + 1000f, heldDirect.z, 2); } // A round with no speed (the weapon authors Velocity 0, or effectively 0) does not fly: it is at its origin at any @@ -95,9 +120,9 @@ public sealed partial class FireControlCut124Tests FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); var shot = SafeAssert.OnlyShot(e.Zone); - var late = FireControl.RoundAt(shot, ShotResult.Miss, shot.FireTime + 5000f); + var late = DrawAhead.Round(shot, Known(shot, ShotResult.Miss), shot.FireOrigin, 0f, shot.FireTime + 5000f, 0f); Assert.Equal(origin.x, late.x, 3); - Assert.Equal(origin.z, late.y, 3); + Assert.Equal(origin.z, late.z, 3); } [Fact] @@ -115,39 +140,44 @@ public sealed partial class FireControlCut124Tests var after = SafeAssert.OnlyShot(e.Zone); Assert.Equal(BallisticSpeed, after.Speed, 4); var aim = normalize(float2(2, 1)); - var at = FireControl.RoundAt(after, null, after.FireTime + 1f); + var at = DrawAhead.Round(after, null, after.FireOrigin, 0f, after.FireTime + 1f, 0f); Assert.Equal(origin.x + aim.x * BallisticSpeed, at.x, 3); - Assert.Equal(origin.z + aim.y * BallisticSpeed, at.y, 3); + Assert.Equal(origin.z + aim.y * BallisticSpeed, at.z, 3); } [Fact] - public void RoundAtIsPure() + public void ARoundIsPure() { - var e = FireBurstRound(out _); + var e = FireDirectRound(20f, 1000f); var shot = SafeAssert.OnlyShot(e.Zone); + e.Target.Direction = normalize(float2(1, 2)); + e.Target.TurnRate = 3f; var time = e.Zone.Time; var entities = e.Zone.Entities.ToList(); var positions = entities.Select(x => x.Position).ToList(); var velocities = entities.Select(x => x.Velocity).ToList(); + var directions = entities.Select(x => x.Direction).ToList(); + var turnRates = entities.Select(x => x.TurnRate).ToList(); var barrel = shot.FireOrigin + float3(1, .5f, -2); - for (var i = 0; i < 25; i++) - { - var at = shot.FireTime + i * .37f; - foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Miss }) + var cell = FarCell(e); + foreach (var known in new[] { null, Known(shot, ShotResult.Miss), Hit(shot, e.Target, cell, shot.CommitTime) }) + for (var i = 0; i < 25; i++) { - FireControl.RoundAt(shot, known, at); - FireControl.RoundEnd(shot, known); - DrawAhead.Round(shot, known, barrel, .1f, at); - DrawAhead.RoundOver(shot, known, at); + var at = shot.FireTime + i * .37f; + DrawAhead.Round(shot, known, barrel, .1f, at, .01f); + DrawAhead.RoundOver(shot, known, at, .01f); + DrawAhead.RoundEnd(shot, known?.Result); + DrawAhead.Impact(e.Target, cell, .01f * i); } - } Assert.Equal(time, e.Zone.Time); Assert.True(e.Zone.PendingShots.Count == 1 && e.Zone.PendingShots[0].Equals(shot), "the pending shot changed"); Assert.True(e.Zone.Entities.SequenceEqual(entities), "the zone's entities changed"); Assert.True(entities.Select(x => x.Position).SequenceEqual(positions), "an entity moved"); Assert.True(entities.Select(x => x.Velocity).SequenceEqual(velocities), "an entity's velocity changed"); + Assert.True(entities.Select(x => x.Direction).SequenceEqual(directions), "an entity turned"); + Assert.True(entities.Select(x => x.TurnRate).SequenceEqual(turnRates), "an entity's turn rate changed"); } // The weapon's Range is frozen with the rest: a round that misses flies on to the range it left with, whatever the stat @@ -158,7 +188,7 @@ public sealed partial class FireControlCut124Tests var e = FireBurstRound(out var origin); var shot = SafeAssert.OnlyShot(e.Zone); Assert.Equal(NoLockRange, shot.MaxRange, 4); - var endBefore = FireControl.RoundEnd(shot, ShotResult.Miss); + var endBefore = DrawAhead.RoundEnd(shot, ShotResult.Miss); // The stat changes after the round left, and the weapon re-reads it. ((InstantWeaponData) e.Weapon.WeaponData).Range = Constant(NoLockRange * 5f); @@ -167,11 +197,11 @@ public sealed partial class FireControlCut124Tests var after = SafeAssert.OnlyShot(e.Zone); Assert.Equal(NoLockRange, after.MaxRange, 4); - Assert.Equal(endBefore, FireControl.RoundEnd(after, ShotResult.Miss), 4); + Assert.Equal(endBefore, DrawAhead.RoundEnd(after, ShotResult.Miss), 4); var aim = normalize(float2(2, 1)); - var held = FireControl.RoundAt(after, ShotResult.Miss, after.FireTime + 1000f); + var held = DrawAhead.Round(after, Known(after, ShotResult.Miss), after.FireOrigin, 0f, after.FireTime + 1000f, 0f); Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); - Assert.Equal(origin.z + aim.y * NoLockRange, held.y, 3); + Assert.Equal(origin.z + aim.y * NoLockRange, held.z, 3); } // A direct round at a stationary target `targetRange` ahead (+z) of the shooter, weapon Range `range`, at `speed`; @@ -198,29 +228,30 @@ public sealed partial class FireControlCut124Tests var d = FireDirectRound(speed, 1000f); var direct = SafeAssert.OnlyShot(d.Zone); Assert.Equal(direct.FireTime + 100f / speed, direct.ArrivalTime, 4); - Assert.Equal(direct.FireTime + 100f / speed, FireControl.RoundEnd(direct, null), 4); - Assert.Equal(direct.FireTime + 100f / speed, FireControl.RoundEnd(direct, ShotResult.Hit), 4); - Assert.Equal(direct.FireTime + 100f / speed, FireControl.RoundEnd(direct, ShotResult.Burst), 4); - Assert.Equal(direct.FireTime + 1000f / speed, FireControl.RoundEnd(direct, ShotResult.Miss), 4); + Assert.Equal(direct.FireTime + 100f / speed, DrawAhead.RoundEnd(direct, null), 4); + Assert.Equal(direct.FireTime + 100f / speed, DrawAhead.RoundEnd(direct, ShotResult.Hit), 4); + Assert.Equal(direct.FireTime + 100f / speed, DrawAhead.RoundEnd(direct, ShotResult.Burst), 4); + Assert.Equal(direct.FireTime + 1000f / speed, DrawAhead.RoundEnd(direct, ShotResult.Miss), 4); - foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Hit, ShotResult.Burst }) - Assert.False(DrawAhead.RoundOver(direct, known, direct.FireTime + 10000f)); + Assert.False(DrawAhead.RoundOver(direct, null, direct.FireTime + 10000f, 0f)); + foreach (var result in new[] { ShotResult.Hit, ShotResult.Burst }) + Assert.False(DrawAhead.RoundOver(direct, Known(direct, result), direct.FireTime + 10000f, 0f)); var missEnd = direct.FireTime + 1000f / speed; - Assert.False(DrawAhead.RoundOver(direct, ShotResult.Miss, missEnd - .01f)); - Assert.True(DrawAhead.RoundOver(direct, ShotResult.Miss, missEnd)); + Assert.False(DrawAhead.RoundOver(direct, Known(direct, ShotResult.Miss), missEnd - .01f, 0f)); + Assert.True(DrawAhead.RoundOver(direct, Known(direct, ShotResult.Miss), missEnd, 0f)); // A target beyond the weapon's range: a miss never ends short of its own arrival. var far = FireDirectRound(speed, 100f, targetRange: 150f); var beyond = SafeAssert.OnlyShot(far.Zone); Assert.Equal(beyond.FireTime + 150f / speed, beyond.ArrivalTime, 4); - Assert.Equal(beyond.FireTime + 150f / speed, FireControl.RoundEnd(beyond, ShotResult.Miss), 4); + Assert.Equal(beyond.FireTime + 150f / speed, DrawAhead.RoundEnd(beyond, ShotResult.Miss), 4); // A round fired at nothing arrives where it left, and a miss flies on to the range. var none = FireDirectRound(speed, 100f, atNothing: true); var nothing = SafeAssert.OnlyShot(none.Zone); Assert.Equal(nothing.FireTime, nothing.ArrivalTime, 4); - Assert.Equal(nothing.FireTime, FireControl.RoundEnd(nothing, null), 4); - Assert.Equal(nothing.FireTime + 100f / speed, FireControl.RoundEnd(nothing, ShotResult.Miss), 4); + Assert.Equal(nothing.FireTime, DrawAhead.RoundEnd(nothing, null), 4); + Assert.Equal(nothing.FireTime + 100f / speed, DrawAhead.RoundEnd(nothing, ShotResult.Miss), 4); } } @@ -239,21 +270,17 @@ public sealed partial class FireControlCut124Tests foreach (var lead in new[] { 0f, step / 2f, .999f * step }) { - var time = shot.ArrivalTime + lead; - foreach (ShotResult? known in new ShotResult?[] { null, ShotResult.Hit, ShotResult.Burst }) - { - var drawn = DrawAhead.Round(shot, known, shot.FireOrigin, 0f, time); - Assert.Equal(origin.x, drawn.x, 3); - Assert.Equal(origin.z + 100f, drawn.z, 3); - } - var missed = DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, 0f, time); + var waiting = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, shot.ArrivalTime, lead); + Assert.Equal(origin.x, waiting.x, 3); + Assert.Equal(origin.z + 100f, waiting.z, 3); + var missed = DrawAhead.Round(shot, Known(shot, ShotResult.Miss), shot.FireOrigin, 0f, shot.ArrivalTime, lead); Assert.Equal(origin.x, missed.x, 3); Assert.Equal(origin.z + 100f + lead * speed, missed.z, 3); } for (var time = shot.FireTime; time <= shot.ArrivalTime + 1f; time += 1f / 144f) { - var drawn = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, time); + var drawn = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, time, 0f); Assert.True(drawn.z - origin.z <= 100f + 1e-3f, "the round was drawn past its arrival point"); } } @@ -289,7 +316,7 @@ public sealed partial class FireControlCut124Tests Assert.Equal(fired.FireTime + 1.05f, record.ArrivalTime, 2); Assert.True(record.ArrivalTime < fired.ArrivalTime); - var stop = DrawAhead.RoundStop(record, outcome, record.FireOrigin, 0f); + var stop = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime, 0f); var expected = outcome.HasBurstPoint ? outcome.BurstPoint : record.FireOrigin.xz + record.TravelDirection * (20f * (record.ArrivalTime - record.FireTime)); @@ -297,7 +324,7 @@ public sealed partial class FireControlCut124Tests Assert.Equal(expected.y, stop.z, 3); foreach (var later in new[] { 0f, .5f, 5f }) { - var drawn = DrawAhead.Round(record, ShotResult.Burst, record.FireOrigin, 0f, record.ArrivalTime + later); + var drawn = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime + later, 0f); Assert.Equal(expected.x, drawn.x, 3); Assert.Equal(expected.y, drawn.z, 3); } @@ -324,11 +351,11 @@ public sealed partial class FireControlCut124Tests Assert.Equal(want.z, got.z, 3); } - Close(barrel, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime)); - Close(Line(blend / 2f) + offset * .5f, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend / 2f)); - Close(Line(blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend)); - Close(Line(2f * blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + 2f * blend)); - Close(Line(0f), DrawAhead.Round(shot, null, barrel, 0f, shot.FireTime)); + Close(barrel, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime, 0f)); + Close(Line(blend / 2f) + offset * .5f, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend / 2f, 0f)); + Close(Line(blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend, 0f)); + Close(Line(2f * blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + 2f * blend, 0f)); + Close(Line(0f), DrawAhead.Round(shot, null, barrel, 0f, shot.FireTime, 0f)); } // Placement depends on sim time alone: any history of frames walked to a checkpoint draws what a fresh call draws, which @@ -342,6 +369,7 @@ public sealed partial class FireControlCut124Tests var shot = SafeAssert.OnlyShot(e.Zone); var checkpoints = new[] { .05f, .3f, 1f, 3f }; var histories = new[] { new[] { 1f / 144f }, new[] { 1f / 30f }, new[] { 1f / 144f, 1f / 30f, 1f / 90f } }; + var miss = Known(shot, ShotResult.Miss); foreach (var frames in histories) { @@ -352,10 +380,18 @@ public sealed partial class FireControlCut124Tests while (elapsed + 1e-4f < checkpoint) { elapsed = min(checkpoint, elapsed + frames[frame++ % frames.Length]); - DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, .1f, shot.FireTime + elapsed); + DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + elapsed, 0f); + } + var drawn = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint, 0f); + var fresh = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint, 0f); + // A frame that falls `lead` after the step draws what the sim time lead later would. + foreach (var lead in new[] { 1f / 60f, 1f / 90f }) + { + var ahead = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint - lead, lead); + Assert.Equal(fresh.x, ahead.x, 3); + Assert.Equal(fresh.y, ahead.y, 3); + Assert.Equal(fresh.z, ahead.z, 3); } - var drawn = DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint); - var fresh = DrawAhead.Round(shot, ShotResult.Miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint); Assert.Equal(fresh.z, drawn.z, 3); Assert.Equal(shot.FireOrigin.z + speed * min(checkpoint, 1000f / speed), drawn.z, 3); Assert.Equal(shot.FireOrigin.x, drawn.x, 3); @@ -364,31 +400,118 @@ public sealed partial class FireControlCut124Tests } } - // Today's sim geometry (a target crossing at 10 against a speed of 20, so an intercept exists): the round flies the line frozen at Fire and stops at the arrival point on it, whatever the - // target does afterwards. (Where a leading round's arrival should lie is the sim's question, never Projectile's.) - [Fact] - public void AHitOnAMovingTargetStopsOnItsFrozenLine() + // A hit ends at its impact cell on the target's drawn pose when damage lands, not at the arrival point of the line + // frozen at Fire (ruling direct-arrival-keep-timing): the target has moved and turned since, and is drawn ahead by its + // last step's velocity and turn. The expected point is computed apart from DrawAhead, by moving the entity itself and + // asking its own schematic frame (Entity.ToWorldPoint) where the cell is. + [Theory] + [InlineData(20f)] + [InlineData(200f)] + public void AHitEndsAtItsImpactCellOnTheTargetsDrawnPose(float speed) { - var e = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, weaponRange: 1000f); + var e = Build(TestSettings(), SolidShape(5, 4), velocity: speed, weaponRange: 1000f); e.Target.Velocity = float2(10, 0); FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); var shot = SafeAssert.OnlyShot(e.Zone); Assert.True(shot.TravelDirection.x > 0f, "the round did not lead the target"); + const float step = 1f / 30f; e.Target.Position += float3(40, 0, 0); - var stop = DrawAhead.RoundStop(shot, new ShotOutcome { ShotId = shot.ShotId, Result = ShotResult.Hit }, shot.FireOrigin, 0f); - var line = shot.FireOrigin.xz + shot.TravelDirection * (BallisticSpeed * (shot.ArrivalTime - shot.FireTime)); - Assert.Equal(line.x, stop.x, 3); - Assert.Equal(shot.FireOrigin.y, stop.y, 3); - Assert.Equal(line.y, stop.z, 3); + e.Target.Direction = normalize(float2(1, 2)); + e.Target.TurnRate = 3f; + var cell = FarCell(e); + var hit = Hit(shot, e.Target, cell, shot.CommitTime); + var arrivalOnTheLine = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, shot.ArrivalTime, 0f); + var cellSize = e.Items.GameplaySettings.SchematicCellSize; + + foreach (var lead in new[] { 0f, step / 2f, .999f * step }) + { + var position = e.Target.Position; + var direction = e.Target.Direction; + e.Target.Position += float3(e.Target.Velocity.x, 0, e.Target.Velocity.y) * lead; + e.Target.Direction = mul(e.Target.Direction, float2x2.Rotate(e.Target.TurnRate * lead)); + var cellPoint = e.Target.ToWorldPoint((float2) cell); + var expected = float3(cellPoint.x, e.Target.Position.y, cellPoint.y); + e.Target.Position = position; + e.Target.Direction = direction; + + Assert.True(length((expected - arrivalOnTheLine).xz) > cellSize, "the fixture put the cell on the frozen line's arrival point"); + var impact = DrawAhead.Impact(e.Target, cell, lead); + Assert.Equal(expected.x, impact.x, 3); + Assert.Equal(expected.y, impact.y, 3); + Assert.Equal(expected.z, impact.z, 3); + foreach (var simTime in new[] { shot.ArrivalTime, shot.ArrivalTime + .5f }) + { + var drawn = DrawAhead.Round(shot, hit, shot.FireOrigin, .1f, simTime, lead); + Assert.Equal(expected.x, drawn.x, 3); + Assert.Equal(expected.y, drawn.y, 3); + Assert.Equal(expected.z, drawn.z, 3); + } + } + } + + // A hit leaves its frozen line at the commit tick and steers onto its end by the time damage lands: before the commit it + // is on the line, half way through the steer it is half way to the impact cell, and a frame drawn ahead by the clock's + // lead places the round where the sim time that much later would. + [Fact] + public void ASteeredRoundLeavesItsLineAtTheCommit() + { + var e = FireDirectRound(20f, 1000f); + var shot = SafeAssert.OnlyShot(e.Zone); + Assert.Equal(shot.FireTime + 5f, shot.ArrivalTime, 3); + var cell = FarCell(e); + var commit = shot.ArrivalTime - 2f; + var hit = Hit(shot, e.Target, cell, commit); + + foreach (var t in new[] { commit - 1f, commit }) + { + var steered = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, t, 0f); + var line = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, t, 0f); + Assert.Equal(line.x, steered.x, 3); + Assert.Equal(line.y, steered.y, 3); + Assert.Equal(line.z, steered.z, 3); + } + + var m = commit + 1f; + var onTheLine = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, m, 0f); + var end = DrawAhead.Impact(e.Target, cell, 0f); + Assert.True(length((end - onTheLine).xz) > 1f, "the fixture put the impact cell on the line"); + var halfway = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, m, 0f); + Assert.Equal(onTheLine.x + (end.x - onTheLine.x) * .5f, halfway.x, 3); + Assert.Equal(onTheLine.y + (end.y - onTheLine.y) * .5f, halfway.y, 3); + Assert.Equal(onTheLine.z + (end.z - onTheLine.z) * .5f, halfway.z, 3); + + var ahead = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, m - .02f, .02f); + Assert.Equal(halfway.x, ahead.x, 3); + Assert.Equal(halfway.y, ahead.y, 3); + Assert.Equal(halfway.z, ahead.z, 3); + } + + // The sim states facts and draws nothing: no drawing function, and no lead, blend or barrel, lives in FireControl; + // those belong to DrawAhead, the presentation statics. + [Fact] + public void FireControlHasNoDrawingFunctions() + { + const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly; + var methods = typeof(FireControl).GetMethods(all).Cast().Concat(typeof(FireControl).GetConstructors(all)).ToList(); + Assert.NotEmpty(methods); + foreach (var method in methods) + { + Assert.False(method.Name.StartsWith("Round") || method.Name.StartsWith("Draw"), "FireControl has a drawing function: " + method.Name); + foreach (var parameter in method.GetParameters()) + Assert.False(new[] { "lead", "blendTime", "barrel" }.Contains(parameter.Name), "FireControl takes a drawing parameter: " + parameter.Name); + } + + foreach (var name in new[] { "Round", "Impact", "RoundOver", "RoundEnd" }) + Assert.NotNull(typeof(DrawAhead).GetMethod(name, BindingFlags.Public | BindingFlags.Static)); } // A proximity round at a target that recedes faster than the round closes bursts at the weapon's range, pulled back // along its line, while it arrives when it has flown the distance the target was fired at: its burst point is not - // the line at its arrival. The stop is the sim's burst point, so a presenter that stopped at the arrival line would + // the line at its arrival. The end is the sim's burst point, so a presenter that ended at the arrival line would // end the tracer short of where the round burst. [Fact] - public void ABurstRoundStopsAtItsBurstPointNotItsArrivalLine() + public void ABurstRoundEndsAtItsBurstPointNotItsArrivalLine() { var e = Build(TestSettings(), SolidShape(5, 4), velocity: BallisticSpeed, fuse: WeaponFuse.Proximity, blastRadius: 4f, damage: 100f, weaponRange: NoLockRange, targetRange: 50f); @@ -404,8 +527,8 @@ public sealed partial class FireControlCut124Tests Assert.Equal(origin.x, outcome.BurstPoint.x, 3); Assert.Equal(origin.z + NoLockRange, outcome.BurstPoint.y, 3); - Assert.Equal(origin.z + 50f, FireControl.RoundAt(record, outcome.Result, record.ArrivalTime).y, 3); - var stop = DrawAhead.RoundStop(record, outcome, record.FireOrigin, 0f); + Assert.Equal(origin.z + 50f, DrawAhead.Round(record, null, record.FireOrigin, 0f, record.ArrivalTime, 0f).z, 3); + var stop = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime, 0f); Assert.Equal(outcome.BurstPoint.x, stop.x, 3); Assert.Equal(outcome.BurstPoint.y, stop.z, 3); } From 6b36835953780ff7c2b3ddd67d9d5e2f1c590665 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:29:32 +0200 Subject: [PATCH 30/49] Ballistic flight r3: name CultMath.float2x2.Rotate in full (the math.float2x2 method shadows the type) The previous commit (2dcb7b20) does not build: DrawAhead.cs CS0119. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/DrawAhead.cs | 2 +- tests/Aetheria.Shared.Tests/BallisticFlightTests.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index 98b70656e..13da8aae9 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -63,7 +63,7 @@ public static class DrawAhead { var at = Position(target, lead); var offset = target.ToWorldPoint((float2) cell) - target.Position.xz; - var turned = mul(offset, float2x2.Rotate(Turn(target, lead))); + var turned = mul(offset, CultMath.float2x2.Rotate(Turn(target, lead))); return float3(at.x + turned.x, at.y, at.z + turned.y); } diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index 61a733e3a..79c787a74 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -9,7 +9,6 @@ using UniRx; using Xunit; using static CultMath.math; using float2 = CultMath.float2; -using float2x2 = CultMath.float2x2; using float3 = CultMath.float3; using int2 = CultMath.int2; @@ -429,7 +428,7 @@ public sealed partial class FireControlCut124Tests var position = e.Target.Position; var direction = e.Target.Direction; e.Target.Position += float3(e.Target.Velocity.x, 0, e.Target.Velocity.y) * lead; - e.Target.Direction = mul(e.Target.Direction, float2x2.Rotate(e.Target.TurnRate * lead)); + e.Target.Direction = mul(e.Target.Direction, CultMath.float2x2.Rotate(e.Target.TurnRate * lead)); var cellPoint = e.Target.ToWorldPoint((float2) cell); var expected = float3(cellPoint.x, e.Target.Position.y, cellPoint.y); e.Target.Position = position; From afe44b09d3ae0f28a5006f9341c36241ec5e6762 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:38:26 +0200 Subject: [PATCH 31/49] ConsumableTests: the overlap test attaches both modifiers before the first expires The first draft activated the second effect so late it was not yet attached when the first detached, so the shared-entry guard mutants survived it. Builds; tests green status is in the next verification run. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ConsumableTests.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs index 843b0f221..a94024c12 100644 --- a/tests/Aetheria.Shared.Tests/ConsumableTests.cs +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -186,8 +186,8 @@ public sealed class ConsumableTests : IDisposable Assert.Equal(40f, rig.Engine.Evaluate(rig.Thrust), 3); } - // Two modifiers share one resolver entry. The first expires while the second still runs: the second's - // boost stays, and when it expires too nothing is left behind. + // Two modifiers share one resolver entry, both attached. The first expires while the second still runs: the + // second's boost stays, and when it expires too nothing is left behind. [Fact] public void AnExpiringConsumableLeavesAnOverlappingOnesBoost() { @@ -199,13 +199,16 @@ public sealed class ConsumableTests : IDisposable Assert.True(rig.Hold.TryStore(rig.Mint())); Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); - Tick(rig, 2, .3f); + Tick(rig, 1, .3f); Assert.True(rig.Ship.TryActivateConsumable(rig.Consumable)); - Tick(rig, 2, .3f); // the first ran out on the fourth tick overall; the second has 0.4 s left + Tick(rig, 2, .3f); // both attached: the first has 0.1 s left, the second 0.4 s + Assert.Equal(40f, rig.Engine.Evaluate(rig.Thrust), 3); + + Tick(rig, 1, .3f); // the first ran out; the second's boost is still on the shared entry Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); Assert.True(resolver.ModifierEntryCount > baseline); - Tick(rig, 2, .3f); + Tick(rig, 1, .3f); // and now the second Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); Assert.Equal(baseline, resolver.ModifierEntryCount); } From a74065dbeeda05feb938c00bf7ec8cef3b4703e3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:46:33 +0200 Subject: [PATCH 32/49] ThrottleLockTests: pin stick length, counter-clockwise turn, non-lock consumables and the lock-end boundary Four tests for Soul's survivors lock-intent-length, lock-ignores-ccw-turn, lock-any-consumable and lock-ends-at-half-duration. Tests only; not yet run on Yggdrasil. Co-Authored-By: Claude Opus 5.5 --- .../ThrottleLockTests.cs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs index 03f7b1060..032e74d0f 100644 --- a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -180,4 +180,92 @@ public sealed class ThrottleLockTests : IDisposable Assert.Equal(1f, f.Forward.Axis); Assert.Equal(0f, f.Reverse.Axis); } + + // Finding zero-stick-unpinned. Mutation: float2(0, length(MovementDirection)) as the locked intent. The lock + // supplies full forward whatever the stick's magnitude, including none and a short reverse push. + [Fact] + public void ALockedShipGetsFullForwardWhateverTheStickLength() + { + var f = Build(100f); + using var _ = f.Cache; + f.ActivateLock(); + foreach (var intent in new[] { float2(0, 0), float2(.3f, .2f), float2(-.5f, -.5f) }) + { + f.Ship.MovementDirection = intent; + f.Ship.Turn = 0; + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Forward.Axis); + Assert.Equal(0f, f.Reverse.Axis); + Assert.All(f.Right, t => Assert.Equal(0f, t.Axis)); + Assert.All(f.Left, t => Assert.Equal(0f, t.Axis)); + } + } + + // Finding ccw-turn-unpinned. Mutation: drop the counter-clockwise Turn. Turn in either sign reaches only the + // thrusters with torque of that sign, at full, while the lock holds forward. + [Fact] + public void ALockedShipTurnsBothWays() + { + var f = Build(100f); + using var _ = f.Cache; + f.ActivateLock(); + var all = f.Ship.GetBehaviors().ToArray(); + var cw = all.Where(t => t.Torque > 0 && t.Item.EquippableItem.Rotation == ItemRotation.None).ToArray(); + var ccw = all.Where(t => t.Torque < 0 && t.Item.EquippableItem.Rotation == ItemRotation.None).ToArray(); + Assert.NotEmpty(cw); + Assert.NotEmpty(ccw); + f.Ship.MovementDirection = float2(1, -1); + foreach (var turn in new[] { 1f, -1f }) + { + f.Ship.Turn = turn; + f.Ship.Update(0.01f); + Assert.All(turn > 0 ? cw : ccw, t => Assert.Equal(1f, t.Axis)); + Assert.All(turn > 0 ? ccw : cw, t => Assert.Equal(0f, t.Axis)); + Assert.Equal(1f, f.Forward.Axis); + } + } + + // Finding non-lock-consumable-unpinned. Mutation: any active consumable locks. A consumable without + // ThrottleLockData leaves the pilot's intent in force. + [Fact] + public void AConsumableWithoutTheLockDoesNotLock() + { + var f = Build(100f); + using var _ = f.Cache; + var plain = new ConsumableItemData { Name = "Plain", Duration = 100f }; + f.Cache.Upsert(plain); + f.Ship.ActivateConsumable(new ConsumableItem + { + Data = f.Cache.RefOf(plain), + Lot = f.Items.Lots.Add(new Lot { Design = f.Cache.RefOf(plain), Origin = new Attributed(), Quality = .5f, Roles = new List() }) + }); + Assert.False(f.Ship.ThrottleLocked); + f.Ship.MovementDirection = float2(0, -1); + f.Ship.Update(0.01f); + Assert.Equal(1f, f.Reverse.Axis); + Assert.Equal(0f, f.Forward.Axis); + } + + // Finding lock-end-boundary-unpinned. Mutation: the lock ends at half Duration. Stepped in quarter-second + // ticks over a 1 s Duration, the lock holds through 0.75 s and is gone by 2 s. The exact tick of expiry is + // consumable-host's rule and is not pinned here. + [Fact] + public void TheLockHoldsThroughMostOfItsDurationAndEndsAfter() + { + var f = Build(1f); + using var _ = f.Cache; + f.ActivateLock(); + f.Ship.MovementDirection = float2(0, -1); + for (var i = 0; i < 3; i++) + { + f.Ship.Update(.25f); + Assert.True(f.Ship.ThrottleLocked); + } + f.Ship.Update(.25f); + Assert.Equal(0f, f.Reverse.Axis); + for (var i = 0; i < 4; i++) f.Ship.Update(.25f); + Assert.False(f.Ship.ThrottleLocked); + f.Ship.Update(.25f); + Assert.Equal(1f, f.Reverse.Axis); + } } From d519d99b1e11f9e3a2379b59ddd24446b046ab1a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 23:48:02 +0200 Subject: [PATCH 33/49] Consumable host: an effect lives exactly its Duration, not Duration plus one tick Expiry fires when the remaining duration reaches zero (<= 0), so behaviours execute on Duration/dt ticks and not one more. AConsumableRunsForExactlyItsDurationInTicks pins it with 0.25 s ticks. Builds; verified in the next run. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Entity.cs | 2 +- tests/Aetheria.Shared.Tests/ConsumableTests.cs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index d7152b172..7d9dfb868 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -1457,7 +1457,7 @@ public abstract class Entity for (var i = 0; i < _activeConsumables.Count; i++) { _activeConsumables[i].Update(delta); - if (_activeConsumables[i].RemainingDuration < 0) + if (_activeConsumables[i].RemainingDuration <= 0) { // Cut 2 Gate 1 fix (docs/stats-and-power-cut.md): an expired consumable dropped out of this // list without ever telling the resolver, leaving its generation/cache/modifier entries diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs index a94024c12..3e2d89db7 100644 --- a/tests/Aetheria.Shared.Tests/ConsumableTests.cs +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -169,6 +169,22 @@ public sealed class ConsumableTests : IDisposable Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); } + // An effect runs for exactly its Duration: Duration 1 with 0.25 s ticks (exact in floats) executes its + // behaviours on four ticks and is gone when the fourth ends, not on a fifth. + [Fact] + public void AConsumableRunsForExactlyItsDurationInTicks() + { + var rig = Build(); + using var _ = rig.Cache; + rig.Ship.ActivateConsumable(rig.Mint()); + Tick(rig, 3, .25f); + Assert.NotNull(rig.Ship.FindActiveConsumable(rig.Consumable)); + Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3); + Tick(rig, 1, .25f); // the fourth tick spends the last of the Duration + Assert.Null(rig.Ship.FindActiveConsumable(rig.Consumable)); + Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3); + } + // CS-R1, the stackable side: a stackable design activates again while its first effect runs, and every // multiplier on the stat multiplies. [Fact] From 840dfe6460b7a0ac424d2a0bb9c0e1af5e61160c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 00:26:21 +0200 Subject: [PATCH 34/49] PBR materials: 12 ambientCG CC0 sets for Unity (GlowFade) under Assets/Materials/PBR 2K-JPG Color/Roughness/NormalGL (+Metalness, AO where the set has them), generated MetallicSmoothness.png (R metallic, A 1-roughness), texture and material .meta files, SOURCES.md. Unity side not opened yet. Co-Authored-By: Claude Opus 5.5 --- Assets/Materials/PBR.meta | 8 ++ Assets/Materials/PBR/Carbon_Fibre.meta | 8 ++ .../PBR/Carbon_Fibre/Carbon_Fibre.mat | 86 +++++++++++++++ .../PBR/Carbon_Fibre/Carbon_Fibre.mat.meta | 8 ++ .../PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg | 3 + .../Carbon_Fibre/Carbon_Fibre_Color.jpg.meta | 104 ++++++++++++++++++ .../Carbon_Fibre_MetallicSmoothness.png | 3 + .../Carbon_Fibre_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Carbon_Fibre/Carbon_Fibre_Metalness.jpg | 3 + .../Carbon_Fibre_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Carbon_Fibre/Carbon_Fibre_NormalGL.jpg | 3 + .../Carbon_Fibre_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Carbon_Fibre/Carbon_Fibre_Roughness.jpg | 3 + .../Carbon_Fibre_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Gunmetal_Dark.meta | 8 ++ .../PBR/Gunmetal_Dark/Gunmetal_Dark.mat | 86 +++++++++++++++ .../PBR/Gunmetal_Dark/Gunmetal_Dark.mat.meta | 8 ++ .../PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg | 3 + .../Gunmetal_Dark_Color.jpg.meta | 104 ++++++++++++++++++ .../Gunmetal_Dark_MetallicSmoothness.png | 3 + .../Gunmetal_Dark_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg | 3 + .../Gunmetal_Dark_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg | 3 + .../Gunmetal_Dark_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg | 3 + .../Gunmetal_Dark_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Metal_Scratched.meta | 8 ++ .../PBR/Metal_Scratched/Metal_Scratched.mat | 86 +++++++++++++++ .../Metal_Scratched/Metal_Scratched.mat.meta | 8 ++ .../Metal_Scratched/Metal_Scratched_Color.jpg | 3 + .../Metal_Scratched_Color.jpg.meta | 104 ++++++++++++++++++ .../Metal_Scratched_MetallicSmoothness.png | 3 + ...etal_Scratched_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Metal_Scratched_Metalness.jpg | 3 + .../Metal_Scratched_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Metal_Scratched_NormalGL.jpg | 3 + .../Metal_Scratched_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Metal_Scratched_Roughness.jpg | 3 + .../Metal_Scratched_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Paint_Clean_Light.meta | 8 ++ .../Paint_Clean_Light/Paint_Clean_Light.mat | 86 +++++++++++++++ .../Paint_Clean_Light.mat.meta | 8 ++ .../Paint_Clean_Light_Color.jpg | 3 + .../Paint_Clean_Light_Color.jpg.meta | 104 ++++++++++++++++++ .../Paint_Clean_Light_MetallicSmoothness.png | 3 + ...nt_Clean_Light_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Paint_Clean_Light_NormalGL.jpg | 3 + .../Paint_Clean_Light_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Paint_Clean_Light_Roughness.jpg | 3 + .../Paint_Clean_Light_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Paint_Worn_Dark.meta | 8 ++ .../PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat | 86 +++++++++++++++ .../Paint_Worn_Dark/Paint_Worn_Dark.mat.meta | 8 ++ .../Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg | 3 + .../Paint_Worn_Dark_AO.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg | 3 + .../Paint_Worn_Dark_Color.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Dark_MetallicSmoothness.png | 3 + ...aint_Worn_Dark_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Paint_Worn_Dark_Metalness.jpg | 3 + .../Paint_Worn_Dark_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Dark_NormalGL.jpg | 3 + .../Paint_Worn_Dark_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Dark_Roughness.jpg | 3 + .../Paint_Worn_Dark_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Paint_Worn_Light.meta | 8 ++ .../PBR/Paint_Worn_Light/Paint_Worn_Light.mat | 86 +++++++++++++++ .../Paint_Worn_Light.mat.meta | 8 ++ .../Paint_Worn_Light/Paint_Worn_Light_AO.jpg | 3 + .../Paint_Worn_Light_AO.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Light_Color.jpg | 3 + .../Paint_Worn_Light_Color.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Light_MetallicSmoothness.png | 3 + ...int_Worn_Light_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Paint_Worn_Light_Metalness.jpg | 3 + .../Paint_Worn_Light_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Light_NormalGL.jpg | 3 + .../Paint_Worn_Light_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Paint_Worn_Light_Roughness.jpg | 3 + .../Paint_Worn_Light_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Plastic_Matte.meta | 8 ++ .../PBR/Plastic_Matte/Plastic_Matte.mat | 86 +++++++++++++++ .../PBR/Plastic_Matte/Plastic_Matte.mat.meta | 8 ++ .../PBR/Plastic_Matte/Plastic_Matte_Color.jpg | 3 + .../Plastic_Matte_Color.jpg.meta | 104 ++++++++++++++++++ .../Plastic_Matte_MetallicSmoothness.png | 3 + .../Plastic_Matte_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Plastic_Matte/Plastic_Matte_NormalGL.jpg | 3 + .../Plastic_Matte_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Plastic_Matte/Plastic_Matte_Roughness.jpg | 3 + .../Plastic_Matte_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Plates_Panels.meta | 8 ++ .../PBR/Plates_Panels/Plates_Panels.mat | 86 +++++++++++++++ .../PBR/Plates_Panels/Plates_Panels.mat.meta | 8 ++ .../PBR/Plates_Panels/Plates_Panels_Color.jpg | 3 + .../Plates_Panels_Color.jpg.meta | 104 ++++++++++++++++++ .../Plates_Panels_MetallicSmoothness.png | 3 + .../Plates_Panels_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Plates_Panels/Plates_Panels_Metalness.jpg | 3 + .../Plates_Panels_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Plates_Panels/Plates_Panels_NormalGL.jpg | 3 + .../Plates_Panels_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Plates_Panels/Plates_Panels_Roughness.jpg | 3 + .../Plates_Panels_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Plates_Riveted.meta | 8 ++ .../PBR/Plates_Riveted/Plates_Riveted.mat | 86 +++++++++++++++ .../Plates_Riveted/Plates_Riveted.mat.meta | 8 ++ .../Plates_Riveted/Plates_Riveted_Color.jpg | 3 + .../Plates_Riveted_Color.jpg.meta | 104 ++++++++++++++++++ .../Plates_Riveted_MetallicSmoothness.png | 3 + ...Plates_Riveted_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Plates_Riveted_Metalness.jpg | 3 + .../Plates_Riveted_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Plates_Riveted_NormalGL.jpg | 3 + .../Plates_Riveted_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Plates_Riveted_Roughness.jpg | 3 + .../Plates_Riveted_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Rubber.meta | 8 ++ Assets/Materials/PBR/Rubber/Rubber.mat | 86 +++++++++++++++ Assets/Materials/PBR/Rubber/Rubber.mat.meta | 8 ++ Assets/Materials/PBR/Rubber/Rubber_Color.jpg | 3 + .../PBR/Rubber/Rubber_Color.jpg.meta | 104 ++++++++++++++++++ .../PBR/Rubber/Rubber_MetallicSmoothness.png | 3 + .../Rubber/Rubber_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Materials/PBR/Rubber/Rubber_NormalGL.jpg | 3 + .../PBR/Rubber/Rubber_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Materials/PBR/Rubber/Rubber_Roughness.jpg | 3 + .../PBR/Rubber/Rubber_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/SOURCES.md | 22 ++++ Assets/Materials/PBR/SOURCES.md.meta | 7 ++ Assets/Materials/PBR/Steel_Brushed.meta | 8 ++ .../PBR/Steel_Brushed/Steel_Brushed.mat | 86 +++++++++++++++ .../PBR/Steel_Brushed/Steel_Brushed.mat.meta | 8 ++ .../PBR/Steel_Brushed/Steel_Brushed_Color.jpg | 3 + .../Steel_Brushed_Color.jpg.meta | 104 ++++++++++++++++++ .../Steel_Brushed_MetallicSmoothness.png | 3 + .../Steel_Brushed_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Steel_Brushed/Steel_Brushed_Metalness.jpg | 3 + .../Steel_Brushed_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Steel_Brushed/Steel_Brushed_NormalGL.jpg | 3 + .../Steel_Brushed_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Steel_Brushed/Steel_Brushed_Roughness.jpg | 3 + .../Steel_Brushed_Roughness.jpg.meta | 104 ++++++++++++++++++ Assets/Materials/PBR/Tread_Diamond.meta | 8 ++ .../PBR/Tread_Diamond/Tread_Diamond.mat | 86 +++++++++++++++ .../PBR/Tread_Diamond/Tread_Diamond.mat.meta | 8 ++ .../PBR/Tread_Diamond/Tread_Diamond_Color.jpg | 3 + .../Tread_Diamond_Color.jpg.meta | 104 ++++++++++++++++++ .../Tread_Diamond_MetallicSmoothness.png | 3 + .../Tread_Diamond_MetallicSmoothness.png.meta | 104 ++++++++++++++++++ .../Tread_Diamond/Tread_Diamond_Metalness.jpg | 3 + .../Tread_Diamond_Metalness.jpg.meta | 104 ++++++++++++++++++ .../Tread_Diamond/Tread_Diamond_NormalGL.jpg | 3 + .../Tread_Diamond_NormalGL.jpg.meta | 104 ++++++++++++++++++ .../Tread_Diamond/Tread_Diamond_Roughness.jpg | 3 + .../Tread_Diamond_Roughness.jpg.meta | 104 ++++++++++++++++++ 157 files changed, 7574 insertions(+) create mode 100644 Assets/Materials/PBR.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg create mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg create mode 100644 Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg create mode 100644 Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Clean_Light.meta create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat.meta create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg create mode 100644 Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg create mode 100644 Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Plastic_Matte.meta create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat.meta create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg create mode 100644 Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Panels.meta create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat.meta create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg create mode 100644 Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg create mode 100644 Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Rubber.meta create mode 100644 Assets/Materials/PBR/Rubber/Rubber.mat create mode 100644 Assets/Materials/PBR/Rubber/Rubber.mat.meta create mode 100644 Assets/Materials/PBR/Rubber/Rubber_Color.jpg create mode 100644 Assets/Materials/PBR/Rubber/Rubber_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg create mode 100644 Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/SOURCES.md create mode 100644 Assets/Materials/PBR/SOURCES.md.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg create mode 100644 Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg.meta create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg create mode 100644 Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg.meta diff --git a/Assets/Materials/PBR.meta b/Assets/Materials/PBR.meta new file mode 100644 index 000000000..a534b0d5a --- /dev/null +++ b/Assets/Materials/PBR.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5b293e272c024dc999b28fe49925e1d5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre.meta b/Assets/Materials/PBR/Carbon_Fibre.meta new file mode 100644 index 000000000..c4c6f51e4 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 636104c9ca48436da05c3f6003fd3eae +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat new file mode 100644 index 000000000..b7f244b60 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Carbon_Fibre + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 5723eb533deb4d73a2b015bb75b523b4, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 32fc9d1c4304495da5350f8a8b52e14b, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: 15a9bba7a82740718ad2d3913f7297af, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat.meta new file mode 100644 index 000000000..b0f1c3097 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8e5a779dec614c80a12925ba063bb99f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg new file mode 100644 index 000000000..4e3c4a374 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:327e42b2dfc859efd9b4f122cf1adbdb611c75e80dd3cb8a18bc69e83eef9585 +size 566952 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg.meta new file mode 100644 index 000000000..614f43088 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 32fc9d1c4304495da5350f8a8b52e14b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png new file mode 100644 index 000000000..49cf03f2e --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a053fa899ea1950563d133010b55a52eee89be8efb3f703e270789a9ad7f75c2 +size 1944565 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png.meta new file mode 100644 index 000000000..19a8fae68 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 15a9bba7a82740718ad2d3913f7297af +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg new file mode 100644 index 000000000..afd0fe3a6 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b65d6b9091747bd81ee58adedcb6209d8ee21b2bde9298d7216989ada85ef622 +size 16545 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta new file mode 100644 index 000000000..2089e25b6 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: b4012db38dfe4d4b8ecea91919903a8f +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg new file mode 100644 index 000000000..c0e1c9aa8 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e76a53fce87086a2346756ef1ef25f6db6153bda3433611a192ac71b32b2ebdc +size 1356999 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg.meta new file mode 100644 index 000000000..072cf13c3 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 5723eb533deb4d73a2b015bb75b523b4 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg new file mode 100644 index 000000000..fd7a1f7f1 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8005027078e974df48351781ce90885bd6325ac63d0a16b41938fca8fd35a634 +size 1189710 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg.meta new file mode 100644 index 000000000..503d1d154 --- /dev/null +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: b8e5cd91353441ebb42296a830fef845 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark.meta b/Assets/Materials/PBR/Gunmetal_Dark.meta new file mode 100644 index 000000000..7680bfe76 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 22e47659e8e14482b3466711751b01b8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat new file mode 100644 index 000000000..150f0f72c --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Gunmetal_Dark + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: dc71e34b4a1f4de7aa826d18b67b5852, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 1ed06708491744fe944217d19e450157, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: 31e7d642ff8a49a28390c82626c4b6b9, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat.meta b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat.meta new file mode 100644 index 000000000..24a52eab3 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 32199fedf9ce473191d8a734a74f3d5f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg new file mode 100644 index 000000000..4a0118922 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28845bf0cc6a1edeed7e9f10f5db4034cae4ba7377bc53633f413de0dbcae5b7 +size 1451112 diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg.meta b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg.meta new file mode 100644 index 000000000..848791f40 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 1ed06708491744fe944217d19e450157 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png new file mode 100644 index 000000000..f3cca5c7f --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c101b3b9d7b3ff0636b6539cc65908dc5cac76e31e951a6a375918bb681076c +size 2899231 diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png.meta new file mode 100644 index 000000000..598a3c189 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 31e7d642ff8a49a28390c82626c4b6b9 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg new file mode 100644 index 000000000..afd0fe3a6 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b65d6b9091747bd81ee58adedcb6209d8ee21b2bde9298d7216989ada85ef622 +size 16545 diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg.meta b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg.meta new file mode 100644 index 000000000..2eb33ad5e --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 51e45f19716b43128ce5d2aac5b5aed4 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg new file mode 100644 index 000000000..37f403c32 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7b4437dca1fe52addbb21e145d3ccc43e8c1780b7930cc9dad1e2b73cb7455b +size 2260227 diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg.meta b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg.meta new file mode 100644 index 000000000..e3fed3ebc --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: dc71e34b4a1f4de7aa826d18b67b5852 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg new file mode 100644 index 000000000..1e6edb81f --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f466bcf997694d56cb7bc93526a881a195e2ee63b1e0c8a19da88289596b9f4 +size 1228992 diff --git a/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg.meta b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg.meta new file mode 100644 index 000000000..d09d43872 --- /dev/null +++ b/Assets/Materials/PBR/Gunmetal_Dark/Gunmetal_Dark_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 5677a8acff8f49358627841b9be8e2dd +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched.meta b/Assets/Materials/PBR/Metal_Scratched.meta new file mode 100644 index 000000000..a53ff5c65 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3e9d00e0b2454cc08632c972d2ee499b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat new file mode 100644 index 000000000..fe6463397 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Metal_Scratched + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 3a10044baac4438f9fc0930783eccdf7, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 5a9cee903a2d48718aaf6b224126f279, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: 8b24d7ea11c341408a146fa39f59b7b0, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat.meta b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat.meta new file mode 100644 index 000000000..1d5895d52 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a5874fba9e2242c79aea6b21ddb63af0 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg new file mode 100644 index 000000000..cb09761f3 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:934e3f0e21b71cb534442f398b287f70158ba7cc3e11ab829f873ddce9cea7fb +size 4314757 diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg.meta b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg.meta new file mode 100644 index 000000000..7b34b1c8f --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 5a9cee903a2d48718aaf6b224126f279 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png new file mode 100644 index 000000000..7b1578437 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ce0400bc23c44f54aa018b195472b99b7c7007bf93166017bfaf05d68bca38d +size 4647897 diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png.meta new file mode 100644 index 000000000..b6b4c4b0e --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 8b24d7ea11c341408a146fa39f59b7b0 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg new file mode 100644 index 000000000..687e06af5 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a4f1124670270e69c5fae2537f2493ab55d6f6264310fa344414eb75026cfde +size 564910 diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg.meta b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg.meta new file mode 100644 index 000000000..697296e8c --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 2eace05436f24d15944266ed3b841f21 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg new file mode 100644 index 000000000..e772289b2 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb3d7953fb7867f7948b5d7a0dff6044f92a6bba56e6f4a48ad30a94c6096d98 +size 5038957 diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg.meta b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg.meta new file mode 100644 index 000000000..932d93b51 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 3a10044baac4438f9fc0930783eccdf7 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg new file mode 100644 index 000000000..9dab37dc6 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fbd2281a995ab1185715dae4d7a657c10b338dfcb57a2f2245eac530d5bd7ed +size 2140434 diff --git a/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg.meta b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg.meta new file mode 100644 index 000000000..393e3bd91 --- /dev/null +++ b/Assets/Materials/PBR/Metal_Scratched/Metal_Scratched_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 986a2475c1e248d7be5208d88ee17a15 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Clean_Light.meta b/Assets/Materials/PBR/Paint_Clean_Light.meta new file mode 100644 index 000000000..c72ff31ca --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c2497e59922c471b935a06c396acb73e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat new file mode 100644 index 000000000..f76052e2d --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Paint_Clean_Light + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: f548cd63b5734e04a284018fefc81b30, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 2c82a94e33b64fd481cbfeb7df9516bd, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: ad8a50784ce04f5d9b3bab2115eeea34, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat.meta b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat.meta new file mode 100644 index 000000000..519f702b0 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 132cb2f99d9f42fe9b05db1c0d653897 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg new file mode 100644 index 000000000..222b441ba --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e966978debc85c05e1e4834e1ac8406d863f9267c396d1580ec2d94fdce9738 +size 638778 diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg.meta b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg.meta new file mode 100644 index 000000000..029f71505 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 2c82a94e33b64fd481cbfeb7df9516bd +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png new file mode 100644 index 000000000..a61363526 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:412547b698a0968ba6f7c4f31e888bd88afb9f24e049f5811ca51c7490510998 +size 3692433 diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png.meta new file mode 100644 index 000000000..41ba9a3be --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: ad8a50784ce04f5d9b3bab2115eeea34 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg new file mode 100644 index 000000000..0c74319d3 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70e36c645d2d6bc9ddc79b86d1275147c52947613fed8ed75dc6aec5d9282187 +size 3220864 diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg.meta b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg.meta new file mode 100644 index 000000000..d0c0e745e --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: f548cd63b5734e04a284018fefc81b30 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg new file mode 100644 index 000000000..499f33718 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44a1a3eb43252db6c242cd3e777a2e41819e872058e032e3cf8285cc578f35bb +size 2011305 diff --git a/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg.meta b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg.meta new file mode 100644 index 000000000..117171aad --- /dev/null +++ b/Assets/Materials/PBR/Paint_Clean_Light/Paint_Clean_Light_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 21721bf50f1f407d8e0f2b5e06bf4ef5 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark.meta b/Assets/Materials/PBR/Paint_Worn_Dark.meta new file mode 100644 index 000000000..f6af35502 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 22c55f60ad784e7495f2ddbae1e2799e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat new file mode 100644 index 000000000..11deee75d --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Paint_Worn_Dark + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP _OCCLUSIONMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: b37cce6dfe8a45d0a7ba4e9a844c68b9, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 8f9bc4377cc4451aae71a877219e700f, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: b5158bc316ef42bead7fa70b7bc08a7a, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 2800000, guid: e8713adbe3e74ac4a4f431581bb31a4b, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat.meta new file mode 100644 index 000000000..c70f30afd --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e081b7d98e6d424fa42a6876a8e0bab3 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg new file mode 100644 index 000000000..7c4f50094 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2e4af5867078208c00332af23bded29a4ac325b040f5980c2730ff8e551bc9d +size 2924087 diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg.meta new file mode 100644 index 000000000..e3c6ca27e --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_AO.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: e8713adbe3e74ac4a4f431581bb31a4b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg new file mode 100644 index 000000000..de88b6108 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c57d484d39db6704fbcf32184f73472e6a78cfc63027422426fa170d87bdfc4a +size 5177435 diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg.meta new file mode 100644 index 000000000..5cdcca076 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 8f9bc4377cc4451aae71a877219e700f +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png new file mode 100644 index 000000000..ca9d86708 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98cc6442c3952acba81f86ae0210e9e0b171522dec1a80d9177eb942dd295d73 +size 5117887 diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png.meta new file mode 100644 index 000000000..28b873752 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: b5158bc316ef42bead7fa70b7bc08a7a +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg new file mode 100644 index 000000000..90096ce14 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3cdb47576c1085bea1bd3e90d055c7f45b66bda53d3ad03f825e5dc9665b93f +size 1073864 diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg.meta new file mode 100644 index 000000000..fb46b8a1a --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: c6d0917aab0d4442ad102a6088628f53 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg new file mode 100644 index 000000000..9c151b688 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60e7d12d4eefed9f9c763b3095b6db35542ee00c7f076b72bb2718a3ccd649c9 +size 7526970 diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg.meta new file mode 100644 index 000000000..bcfcf9af3 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: b37cce6dfe8a45d0a7ba4e9a844c68b9 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg new file mode 100644 index 000000000..918bb48c5 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:648bf332066975dad0fc618eda260571e8077a7d418587b8b55a68c7da797541 +size 2083327 diff --git a/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg.meta new file mode 100644 index 000000000..3607f105d --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Dark/Paint_Worn_Dark_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 46fe06d220b044d1a6bb1a716c6e8705 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light.meta b/Assets/Materials/PBR/Paint_Worn_Light.meta new file mode 100644 index 000000000..34d26b097 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b660a03d95714359ac68aa01a52064b9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat new file mode 100644 index 000000000..9f6a4ff51 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Paint_Worn_Light + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP _OCCLUSIONMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 4ce3c2cc69304068b4a2ddc833b748b9, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 73568a0c02bd4184bcb398c9e3dd7ab6, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: d9cd336372c244e398532512e3f8f3be, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 2800000, guid: 73e117729c3447e5aa0eb1a30fad7c75, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat.meta new file mode 100644 index 000000000..1441ced17 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5eb890ad19d44af98f2710f3af280e3c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg new file mode 100644 index 000000000..a9989d282 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2560f1aa453ffd2f4374c9ba4d184e1c43456726d8971460062515acef83b76 +size 2764255 diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg.meta new file mode 100644 index 000000000..be48781f6 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_AO.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 73e117729c3447e5aa0eb1a30fad7c75 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg new file mode 100644 index 000000000..d05a21ab2 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:401b7ee4394595110564bcb12a40c0b87ea50acfc3632b7e7237e2d0165d1ded +size 4364255 diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg.meta new file mode 100644 index 000000000..cc5b0e17b --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 73568a0c02bd4184bcb398c9e3dd7ab6 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png new file mode 100644 index 000000000..08375c3a3 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c59489103f8a5ef438f0f74484cdf5af90b1ee2985a180d690772d1325816bf +size 6990606 diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png.meta new file mode 100644 index 000000000..13b040de5 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: d9cd336372c244e398532512e3f8f3be +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg new file mode 100644 index 000000000..10a0cd502 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8be2891eeff9e521ac8d672edb106830e878ee0b73852a5763c803a8088014f2 +size 2705735 diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg.meta new file mode 100644 index 000000000..402c2f3f3 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 464aecd85593406aa2426215e07b988a +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg new file mode 100644 index 000000000..549164ad2 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2ae51f315549a7d63c7f0290e9f6d35dfcd1bc42df422579ee407c30692599a +size 6857447 diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg.meta new file mode 100644 index 000000000..a9aa1c988 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 4ce3c2cc69304068b4a2ddc833b748b9 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg new file mode 100644 index 000000000..d0ab06978 --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1207c0c4716229b36290416e4af37ccaf5351a65c8d93237a7802a95f45dfbcf +size 1929057 diff --git a/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg.meta b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg.meta new file mode 100644 index 000000000..2b1eeabdb --- /dev/null +++ b/Assets/Materials/PBR/Paint_Worn_Light/Paint_Worn_Light_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 58cb097056b5431e819ca45954ee3b47 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plastic_Matte.meta b/Assets/Materials/PBR/Plastic_Matte.meta new file mode 100644 index 000000000..91fd17d97 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1126ae7b5bcb4d6b8403e31233638b8d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat new file mode 100644 index 000000000..f5845363d --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Plastic_Matte + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: d9e9439b6e9f43ed9c47015cafdcd5d3, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 053c9b821bf649bab9c8f7b234803809, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: f405c1ca89914831a4366ac1e4d850ee, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat.meta b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat.meta new file mode 100644 index 000000000..f92b559c4 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 636667df941046e79d2417c6b062d995 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg new file mode 100644 index 000000000..00c2a444a --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cb3a446755c2f868957807195d1e51ff989384da7878e8c0d9f93f391a5b755 +size 905715 diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg.meta b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg.meta new file mode 100644 index 000000000..e0947dee4 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 053c9b821bf649bab9c8f7b234803809 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png new file mode 100644 index 000000000..45e075a5b --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:811b05a5b78048c711f5129d14073bfa1983443553a2b75453f15e46ad6da998 +size 3782071 diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png.meta new file mode 100644 index 000000000..c641103a4 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: f405c1ca89914831a4366ac1e4d850ee +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg new file mode 100644 index 000000000..12b6ad1ff --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e177082ed5027b4bb533e39b0556c7f3ca4701d6ffa3761910dff678d7f43d07 +size 9096346 diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg.meta b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg.meta new file mode 100644 index 000000000..357ce2800 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: d9e9439b6e9f43ed9c47015cafdcd5d3 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg new file mode 100644 index 000000000..c1eb5f150 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:629c695383835080794b90174ae19a422cb7234870752642afa81f36a28cd9e7 +size 2095946 diff --git a/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg.meta b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg.meta new file mode 100644 index 000000000..575bf2428 --- /dev/null +++ b/Assets/Materials/PBR/Plastic_Matte/Plastic_Matte_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 142ed10762d242a9a4ecf63ee0e38b71 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels.meta b/Assets/Materials/PBR/Plates_Panels.meta new file mode 100644 index 000000000..99eb09f70 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 086928e6583747e1b14c6767ced082b4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat b/Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat new file mode 100644 index 000000000..babe13ca0 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Plates_Panels + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 8bc10cfce8a64defaf9f67d156003f09, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 2195885db9e846999fd10aa7f5c629b1, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: d91bff887b764847a484d0a9bedce9e9, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat.meta b/Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat.meta new file mode 100644 index 000000000..a8a703cf0 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c508beb10bbe4c7f88fc9a7c003e51d5 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg new file mode 100644 index 000000000..9945f16ae --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e69514467f58898537ecc6b3c47e618537406a00b7eb24099d3835ca4ae4c9a +size 2132182 diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg.meta b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg.meta new file mode 100644 index 000000000..960e6e0e3 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 2195885db9e846999fd10aa7f5c629b1 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png new file mode 100644 index 000000000..de028ea73 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b5ea65058b4792f97c7f9affd89930342b2306ab5dbd21f930ef153d4f4acf1 +size 3414650 diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png.meta new file mode 100644 index 000000000..6b036cb95 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: d91bff887b764847a484d0a9bedce9e9 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg new file mode 100644 index 000000000..9edfab941 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:736a312c106a791672e1115df51ba0a08be7927f6cfc4aff97fcfb60311e002f +size 171958 diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg.meta b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg.meta new file mode 100644 index 000000000..69399ee42 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 5df39678e5734f51a29940f2f6a43dda +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg new file mode 100644 index 000000000..127c7888d --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f916040ca388073191224a515def08761a3e136a74f559feb5774ef50527d54 +size 3505435 diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg.meta b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg.meta new file mode 100644 index 000000000..da4e2137e --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 8bc10cfce8a64defaf9f67d156003f09 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg new file mode 100644 index 000000000..cf7a6709b --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9fc8550cdb559c16cbd2d43196dcc271653e78c802519bc446684224048ff69 +size 1742286 diff --git a/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg.meta b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg.meta new file mode 100644 index 000000000..9d51593f6 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Panels/Plates_Panels_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: e2f6e4f21fd14d6eb65e100b4f012349 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted.meta b/Assets/Materials/PBR/Plates_Riveted.meta new file mode 100644 index 000000000..870fc938a --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 79fbea05234f4b53a53eab2f090d2005 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat new file mode 100644 index 000000000..e8088be9b --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Plates_Riveted + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 6fb688a276034675b895c2edeb1b538b, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: f07bacf217654b269eecfabbcc651b2b, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: 08b45a54ea8c4521b7c4e7b3974f0b42, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat.meta b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat.meta new file mode 100644 index 000000000..62c10b5e6 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 538d82fdc2de48f4bcf4e93b17407f90 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg new file mode 100644 index 000000000..fe22d7428 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a72ee2d4e5e6c3d4154e9244ad57f7c6b644b40b356db162c9f2b01e80547eb +size 1602784 diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg.meta b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg.meta new file mode 100644 index 000000000..dc74b1c54 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: f07bacf217654b269eecfabbcc651b2b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png new file mode 100644 index 000000000..527fe9f5f --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d678e8bff66b2d3bc703c5d355c47644f135544ac33afd517f1a542195b7e43d +size 5021835 diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png.meta new file mode 100644 index 000000000..778892894 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 08b45a54ea8c4521b7c4e7b3974f0b42 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg new file mode 100644 index 000000000..05c05f99a --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:212258a43acddd1cb67203b3b263721022ae5f22bbf2213842a608fb09dc2b2f +size 1396144 diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg.meta b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg.meta new file mode 100644 index 000000000..86f483ff4 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 24e3f6b76da9420592e2f21931defae7 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg new file mode 100644 index 000000000..4c272122c --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e49f90c003cf728de4892eec7d71f08abec1b2f9cb4780c875d005cb313498ac +size 3616644 diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg.meta b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg.meta new file mode 100644 index 000000000..aa8d94dbe --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 6fb688a276034675b895c2edeb1b538b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg new file mode 100644 index 000000000..b96f7460a --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d48b683b55fd5e89c91a0c4bfde3a539fc892abc958387045e7d0786fe5c1fbd +size 1889081 diff --git a/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg.meta b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg.meta new file mode 100644 index 000000000..232a09cf3 --- /dev/null +++ b/Assets/Materials/PBR/Plates_Riveted/Plates_Riveted_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 7b50b58679684fc8991f112974a62120 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Rubber.meta b/Assets/Materials/PBR/Rubber.meta new file mode 100644 index 000000000..f4d39fafb --- /dev/null +++ b/Assets/Materials/PBR/Rubber.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 180d9a65c5784b618970824077421002 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Rubber/Rubber.mat b/Assets/Materials/PBR/Rubber/Rubber.mat new file mode 100644 index 000000000..2c10c3488 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Rubber + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 42826f488ae243178be306ada5ee9f41, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: 83e6c1768e014acfb494fbf007c40c80, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: f1d4409638e4452cb1412503ed8526de, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Rubber/Rubber.mat.meta b/Assets/Materials/PBR/Rubber/Rubber.mat.meta new file mode 100644 index 000000000..989c2cd22 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 257d49e9af154d6c9b5ea50a288db1fa +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Rubber/Rubber_Color.jpg b/Assets/Materials/PBR/Rubber/Rubber_Color.jpg new file mode 100644 index 000000000..3a0e09062 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d712c25987d3e6f40e3f37fae1ce1a4a4155d5e27cff64dadf2bbe140979dd98 +size 4479634 diff --git a/Assets/Materials/PBR/Rubber/Rubber_Color.jpg.meta b/Assets/Materials/PBR/Rubber/Rubber_Color.jpg.meta new file mode 100644 index 000000000..ff443d7b4 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 83e6c1768e014acfb494fbf007c40c80 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png b/Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png new file mode 100644 index 000000000..d194ddc8f --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a5d2c567edceeeec37de50166d325a0b3d29bc6cd90ad891635bf4cf8334376 +size 5044494 diff --git a/Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png.meta new file mode 100644 index 000000000..fe4234492 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: f1d4409638e4452cb1412503ed8526de +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg b/Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg new file mode 100644 index 000000000..461b856ff --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61bf1b38eae723913275a1de5eb08d4ac543f11e28f4ac6f596a0644169010fc +size 9238212 diff --git a/Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg.meta b/Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg.meta new file mode 100644 index 000000000..16144739a --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 42826f488ae243178be306ada5ee9f41 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg b/Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg new file mode 100644 index 000000000..7c59cbb40 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc137f95179a4ae42d67819750d78c3fe878d438b789cf96b33947618305a275 +size 3042709 diff --git a/Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg.meta b/Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg.meta new file mode 100644 index 000000000..feca8c0c2 --- /dev/null +++ b/Assets/Materials/PBR/Rubber/Rubber_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 834b5a5e1e1a49559f9fdeffd947e0a0 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/SOURCES.md b/Assets/Materials/PBR/SOURCES.md new file mode 100644 index 000000000..d0f2c96cb --- /dev/null +++ b/Assets/Materials/PBR/SOURCES.md @@ -0,0 +1,22 @@ +# PBR material sources + +All texture sets are from ambientCG (https://ambientcg.com), licensed CC0 1.0 Universal (public domain, https://creativecommons.org/publicdomain/zero/1.0/). 2K-JPG downloads; displacement and NormalDX dropped; NormalGL kept (OpenGL convention, used by Blender and Unity). + +| Material | ambientCG id | URL | Metallic source | +|---|---|---|---| +| Paint_Clean_Light | Plastic010 | https://ambientcg.com/view?id=Plastic010 | none in set: constant 0 | +| Paint_Worn_Light | PaintedMetal012 | https://ambientcg.com/view?id=PaintedMetal012 | ambientCG Metalness map | +| Paint_Worn_Dark | PaintedMetal007 | https://ambientcg.com/view?id=PaintedMetal007 | ambientCG Metalness map | +| Steel_Brushed | Metal009 | https://ambientcg.com/view?id=Metal009 | ambientCG Metalness map | +| Metal_Scratched | Metal002 | https://ambientcg.com/view?id=Metal002 | ambientCG Metalness map | +| Gunmetal_Dark | Metal030 | https://ambientcg.com/view?id=Metal030 | ambientCG Metalness map | +| Plates_Riveted | MetalPlates002 | https://ambientcg.com/view?id=MetalPlates002 | ambientCG Metalness map | +| Plates_Panels | MetalPlates006 | https://ambientcg.com/view?id=MetalPlates006 | ambientCG Metalness map | +| Tread_Diamond | DiamondPlate001 | https://ambientcg.com/view?id=DiamondPlate001 | ambientCG Metalness map | +| Rubber | Rubber001 | https://ambientcg.com/view?id=Rubber001 | none in set: constant 0 | +| Plastic_Matte | Plastic004 | https://ambientcg.com/view?id=Plastic004 | none in set: constant 0 | +| Carbon_Fibre | Fabric004 | https://ambientcg.com/view?id=Fabric004 | ambientCG Metalness map | + +Files per material: `_Color`, `_Roughness`, `_NormalGL`, `_Metalness` and `_AO` (where the set has them), plus `_MetallicSmoothness.png` generated here for Unity (Metallic in R, Smoothness = 1 - Roughness in A). AO exists only for Paint_Worn_Light and Paint_Worn_Dark. + +Blender library: `Asset Sources/PBR Materials.blend` (catalog Aetheria/PBR, images linked by relative path to this folder). Unity materials use Aetheria/GlowFade. diff --git a/Assets/Materials/PBR/SOURCES.md.meta b/Assets/Materials/PBR/SOURCES.md.meta new file mode 100644 index 000000000..4c16a4a62 --- /dev/null +++ b/Assets/Materials/PBR/SOURCES.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a29568b9a0cf46babc43fe6045d6e10a +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed.meta b/Assets/Materials/PBR/Steel_Brushed.meta new file mode 100644 index 000000000..0a76b67a2 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 76edddc4bbd74a8d80e7cc29729b334e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat new file mode 100644 index 000000000..7147c9aa2 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Steel_Brushed + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: c3ef09438e3e46d18a4c267810aeb285, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: f70b6df0395b4752b4ec80cb7bfd8f42, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: 40f5e915d2734d43bc01f984772e9d29, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat.meta b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat.meta new file mode 100644 index 000000000..6a17f81b3 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 65c607f056d64708bb493dd31b39e79e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg new file mode 100644 index 000000000..c3406774a --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:209d3bb050be9f58c5af179d9712fa7799567a4cd50afd338d37674bdcff765b +size 2439272 diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg.meta b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg.meta new file mode 100644 index 000000000..d6973f061 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: f70b6df0395b4752b4ec80cb7bfd8f42 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png new file mode 100644 index 000000000..8142b06f2 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2b84e6acb67eb71fb263546ba5fe2ddd5f6e00517addb3c96c0f226becab3bb +size 3309438 diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png.meta new file mode 100644 index 000000000..130146909 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 40f5e915d2734d43bc01f984772e9d29 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg new file mode 100644 index 000000000..afd0fe3a6 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b65d6b9091747bd81ee58adedcb6209d8ee21b2bde9298d7216989ada85ef622 +size 16545 diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg.meta b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg.meta new file mode 100644 index 000000000..1aa819912 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 415d2498454d4d419948b2aa11eac78c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg new file mode 100644 index 000000000..2c9942542 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8773d9e97a4b057ced3e538d8ecc6069d27c8948191d61daf79b9ad93d93e3b3 +size 4120356 diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg.meta b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg.meta new file mode 100644 index 000000000..e223398fa --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: c3ef09438e3e46d18a4c267810aeb285 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg new file mode 100644 index 000000000..a1d7edd1f --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e802f013b93cb1bd4b55cd91f02a1863233475a7f8bd0e6755cfaa447a729674 +size 1891607 diff --git a/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg.meta b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg.meta new file mode 100644 index 000000000..726ff5549 --- /dev/null +++ b/Assets/Materials/PBR/Steel_Brushed/Steel_Brushed_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 158c7794c28740d0a16fa2dc22e93d8c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond.meta b/Assets/Materials/PBR/Tread_Diamond.meta new file mode 100644 index 000000000..6a3445907 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 78b78bf7d2f84a8d85daaf75d21fa56f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat new file mode 100644 index 000000000..5b9542d0f --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat @@ -0,0 +1,86 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Tread_Diamond + m_Shader: {fileID: 4800000, guid: ddff0b48b92c991499817d2a0f5d5059, type: 3} + m_ShaderKeywords: _MAINTEX _METALLICGLOSSMAP _NORMALMAP + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 2800000, guid: 043636050ee1492697e5bebb7603bf12, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _FadeTex: + m_Texture: {fileID: 2800000, guid: df0632a08c5f1a1498fa325c755dc459, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 2800000, guid: f04f0c584e824d81b7d7a91b969efef8, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 2800000, guid: 28456bc0a3c045e394e0937b6fe0231c, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _Fade: 0 + - _FadeEdge: 0.05 + - _FadeSharpness: 16 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EdgeColor: {r: 3.0117648, g: 3.0117648, b: 11.984314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat.meta b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat.meta new file mode 100644 index 000000000..dd3e8ee22 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bd8a08731d8c4a4d8961c92fe318f1bb +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg new file mode 100644 index 000000000..c4aae91a2 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a1de0119ef8592c3054949b6a9608a5a814156f661ed54541b390feb6fb87cd +size 2675454 diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg.meta b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg.meta new file mode 100644 index 000000000..f02278e36 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Color.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: f04f0c584e824d81b7d7a91b969efef8 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png new file mode 100644 index 000000000..8c48d3028 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6131476bdbc8b6e910a5537a0aaadc977584f22c2a76d97f4cab4aa2b2b7ec0 +size 7151369 diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png.meta b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png.meta new file mode 100644 index 000000000..8804c2d58 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_MetallicSmoothness.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 28456bc0a3c045e394e0937b6fe0231c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg new file mode 100644 index 000000000..d65027465 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6925202668aae7828b0a4765ca8854c539d94c4cd0f945b5195b5576272cdc7a +size 2420905 diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg.meta b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg.meta new file mode 100644 index 000000000..c786edbd3 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Metalness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 5086225cf1bf485597fe191f1b9ca912 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg new file mode 100644 index 000000000..7411d45d0 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:317627265f2587cdfb00e06ea80415333b0ff2597321c79a77c2fd55448e5ec5 +size 7454546 diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg.meta b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg.meta new file mode 100644 index 000000000..747097254 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_NormalGL.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 043636050ee1492697e5bebb7603bf12 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 1 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg new file mode 100644 index 000000000..2a2b7c7f8 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6aa2c8bd6044829215771cb1c3d82fcd3845565ff1a9416f4a10403dc2dfbc1b +size 2395648 diff --git a/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg.meta b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg.meta new file mode 100644 index 000000000..cc15cc441 --- /dev/null +++ b/Assets/Materials/PBR/Tread_Diamond/Tread_Diamond_Roughness.jpg.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 1d942e92edc64885ae82ceb88af83104 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 4 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: From 492a8333fb4aeb07648c78e7eb31d0e2d1043dfd Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 00:26:22 +0200 Subject: [PATCH 35/49] Blender asset library: PBR Materials.blend (catalog Aetheria/PBR) 12 asset-marked materials, UV mapping with a scalar Tiling node, images linked by relative path to Assets/Materials/PBR, previews generated. Co-Authored-By: Claude Opus 5.5 --- Asset Sources/PBR Materials.blend | 3 +++ Asset Sources/blender_assets.cats.txt | 10 ++++++++++ 2 files changed, 13 insertions(+) create mode 100644 Asset Sources/PBR Materials.blend create mode 100644 Asset Sources/blender_assets.cats.txt diff --git a/Asset Sources/PBR Materials.blend b/Asset Sources/PBR Materials.blend new file mode 100644 index 000000000..445390b60 --- /dev/null +++ b/Asset Sources/PBR Materials.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f86c6a34ead2264030e7a54f276229e0d84d90f9c09db25fe31c1c086cc933c7 +size 356054 diff --git a/Asset Sources/blender_assets.cats.txt b/Asset Sources/blender_assets.cats.txt new file mode 100644 index 000000000..0dc908483 --- /dev/null +++ b/Asset Sources/blender_assets.cats.txt @@ -0,0 +1,10 @@ +# This is an Asset Catalog Definition file for Blender. +# +# Empty lines and lines starting with `#` will be ignored. +# The first non-ignored line should be the version indicator. +# Other lines are of the format "UUID:catalog/path/for/assets:simple catalog name" + +VERSION 1 + +6a165bb4-35a8-42c5-904e-cea20c966e8e:Aetheria:Aetheria +815f70ff-5f5c-4582-b612-a6ca360fcc59:Aetheria/PBR:Aetheria-PBR From 80842ba2fc362d1066e715a988a1b2e87bd7abcb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 00:27:53 +0200 Subject: [PATCH 36/49] Carbon_Fibre: dielectric, metallic 0 (drop unused Metalness map) Co-Authored-By: Claude Opus 5.5 --- Asset Sources/PBR Materials.blend | 4 +- Asset Sources/blender_assets.cats.txt | 4 +- .../Carbon_Fibre_MetallicSmoothness.png | 4 +- .../Carbon_Fibre/Carbon_Fibre_Metalness.jpg | 3 - .../Carbon_Fibre_Metalness.jpg.meta | 104 ------------------ Assets/Materials/PBR/SOURCES.md | 2 +- 6 files changed, 7 insertions(+), 114 deletions(-) delete mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg delete mode 100644 Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta diff --git a/Asset Sources/PBR Materials.blend b/Asset Sources/PBR Materials.blend index 445390b60..cbe7a475b 100644 --- a/Asset Sources/PBR Materials.blend +++ b/Asset Sources/PBR Materials.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f86c6a34ead2264030e7a54f276229e0d84d90f9c09db25fe31c1c086cc933c7 -size 356054 +oid sha256:7c88c4c5380c3553182a545e04ffcc0776a54f7cbde4fa80de95f5e569b931de +size 359499 diff --git a/Asset Sources/blender_assets.cats.txt b/Asset Sources/blender_assets.cats.txt index 0dc908483..7fc29fe08 100644 --- a/Asset Sources/blender_assets.cats.txt +++ b/Asset Sources/blender_assets.cats.txt @@ -6,5 +6,5 @@ VERSION 1 -6a165bb4-35a8-42c5-904e-cea20c966e8e:Aetheria:Aetheria -815f70ff-5f5c-4582-b612-a6ca360fcc59:Aetheria/PBR:Aetheria-PBR +2c70f9d2-3a69-4004-8f42-4712b3e6c439:Aetheria:Aetheria +a71be0dc-105e-4c66-8ff3-acb25522e9cd:Aetheria/PBR:Aetheria-PBR diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png index 49cf03f2e..31131f1a7 100644 --- a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png +++ b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_MetallicSmoothness.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a053fa899ea1950563d133010b55a52eee89be8efb3f703e270789a9ad7f75c2 -size 1944565 +oid sha256:fd50d0187fdf8c05eba5ef982da5217c6adf9f03720a2c8041ef4bebd5a2658d +size 1944562 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg deleted file mode 100644 index afd0fe3a6..000000000 --- a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b65d6b9091747bd81ee58adedcb6209d8ee21b2bde9298d7216989ada85ef622 -size 16545 diff --git a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta b/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta deleted file mode 100644 index 2089e25b6..000000000 --- a/Assets/Materials/PBR/Carbon_Fibre/Carbon_Fibre_Metalness.jpg.meta +++ /dev/null @@ -1,104 +0,0 @@ -fileFormatVersion: 2 -guid: b4012db38dfe4d4b8ecea91919903a8f -TextureImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 13 - mipmaps: - mipMapMode: 0 - enableMipMap: 1 - sRGBTexture: 0 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - flipGreenChannel: 0 - isReadable: 0 - streamingMipmaps: 0 - streamingMipmapsPriority: 0 - vTOnly: 0 - ignoreMipmapLimit: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: 1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: 1 - aniso: 4 - mipBias: 0 - wrapU: 0 - wrapV: 0 - wrapW: 0 - nPOTScale: 1 - lightmap: 0 - compressionQuality: 50 - spriteMode: 0 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 100 - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spriteGenerateFallbackPhysicsShape: 1 - alphaUsage: 1 - alphaIsTransparency: 0 - spriteTessellationDetail: -1 - textureType: 0 - textureShape: 1 - singleChannelComponent: 0 - flipbookRows: 1 - flipbookColumns: 1 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - ignorePngGamma: 0 - applyGammaDecoding: 0 - swizzle: 50462976 - cookieLightType: 1 - platformSettings: - - serializedVersion: 4 - buildTarget: DefaultTexturePlatform - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - spriteSheet: - serializedVersion: 2 - sprites: [] - outline: [] - customData: - physicsShape: [] - bones: [] - spriteID: - internalID: 0 - vertices: [] - indices: - edges: [] - weights: [] - secondaryTextures: [] - spriteCustomMetadata: - entries: [] - nameFileIdTable: {} - mipmapLimitGroupName: - pSDRemoveMatte: 0 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Materials/PBR/SOURCES.md b/Assets/Materials/PBR/SOURCES.md index d0f2c96cb..baebd4022 100644 --- a/Assets/Materials/PBR/SOURCES.md +++ b/Assets/Materials/PBR/SOURCES.md @@ -15,7 +15,7 @@ All texture sets are from ambientCG (https://ambientcg.com), licensed CC0 1.0 Un | Tread_Diamond | DiamondPlate001 | https://ambientcg.com/view?id=DiamondPlate001 | ambientCG Metalness map | | Rubber | Rubber001 | https://ambientcg.com/view?id=Rubber001 | none in set: constant 0 | | Plastic_Matte | Plastic004 | https://ambientcg.com/view?id=Plastic004 | none in set: constant 0 | -| Carbon_Fibre | Fabric004 | https://ambientcg.com/view?id=Fabric004 | ambientCG Metalness map | +| Carbon_Fibre | Fabric004 | https://ambientcg.com/view?id=Fabric004 | set map is all 1.0; dielectric, so constant 0 | Files per material: `_Color`, `_Roughness`, `_NormalGL`, `_Metalness` and `_AO` (where the set has them), plus `_MetallicSmoothness.png` generated here for Unity (Metallic in R, Smoothness = 1 - Roughness in A). AO exists only for Paint_Worn_Light and Paint_Worn_Dark. From ce0023f1f8b47f8a60978702c35f2cc2f4e80c33 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 00:50:14 +0200 Subject: [PATCH 37/49] Research: prior art on hull decals, panel lines and wear layering Eyes pass before the hull shader fork: DECALmachine 2.17 export paths, forward-renderer decal practice, parallax for panel lines, layered wear on the built-in pipeline, and this project's rendering constraints. Co-Authored-By: Claude Opus 5.5 --- .../hull-decals-and-wear-prior-art.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/research/hull-decals-and-wear-prior-art.md diff --git a/docs/research/hull-decals-and-wear-prior-art.md b/docs/research/hull-decals-and-wear-prior-art.md new file mode 100644 index 000000000..e8e62a16a --- /dev/null +++ b/docs/research/hull-decals-and-wear-prior-art.md @@ -0,0 +1,70 @@ +# Hull decals, panel lines and wear layering: prior art + +Eyes pass, 2026-10-10, before the hull shader design fork (operator: "I want tiling materials with global effects on top, plus decals ... Needs a new shader in unity to apply parallax decals for nice panel lines"). Related release-map follow-ups: hull-decals, hull-damage-wear. Facts only; tags mark FETCHED, SEARCH and KNOWLEDGE (unverified). + +# Hull decals and wear: prior-art facts (Eyes, 2026-10-10) + +Target (operator): tiling PBR materials + global per-ship wear/dirt masks + parallax panel-line decals, authored with DECALmachine 2.17 (Blender 5.2). +Tags: FETCHED = page read this session; SEARCH = search-result summary only; KNOWLEDGE = unverified recall; PROJECT = read from origin/master. +Placement: F:\Projects\Aetheria has no docs/research on origin/master, so this file sits in the scratchpad for Self to commit. + +## 5. Project / Unity constraints (PROJECT, origin/master; ProjectSettings are LFS pointers, read via `git lfs smudge`) +- Unity 6000.3.24f1 (ProjectVersion.txt). Built-in pipeline: GraphicsSettings m_CustomRenderPipeline {fileID: 0}. +- Colour space linear: ProjectSettings m_ActiveColorSpace: 1. +- GraphicsSettings: m_DefaultRenderingPath: 1, m_DefaultMobileRenderingPath: 1, tier settings (m_BuildTarget 1, Tier 2) renderingPath: 1, useHDR: 1, useDetailNormalMap: 1, standardShaderQuality 2. (Enum reading, KNOWLEDGE: 1 = Forward.) +- Per-camera override, counted in scenes/prefabs: m_RenderingPath: 1 on 21 cameras, -1 (use project setting) on 5, 3 (Deferred) on 4: Assets/Scenes/ARPG.unity:30582, FieldShieldTest.unity:1695, Main Menu.unity:1258, TestScene.unity:3781. Not checked: which camera ARPG:30582 is, or whether it is the gameplay camera. Effective path is forward for most cameras, deferred on those four. +- QualitySettings: single level "Amazing", pixelLightCount 4, shadows 0, shadowCascades 4, shadowDistance 150, anisotropicTextures 0, textureQuality 0. +- Graphics APIs set: Windows m_APIs 0200000012000000 (KNOWLEDGE decode: 2 = D3D11, 0x12 = Vulkan), Android 0b000000 (KNOWLEDGE: GLES3), iOS Metal; not decoded further. +- GlowFade (Assets/Shaders/GlowFade.shader, "Aetheria/GlowFade") is a CGPROGRAM surface shader: `#pragma surface surf Standard fullforwardshadows noambient vertex:vert`, `#pragma target 3.0`, Cull Off, RenderType Opaque, FallBack Diffuse. + - Samplers declared: _MainTex, _MetallicGlossMap, _OcclusionMap, _FadeTex, _BumpMap (5). All except _FadeTex sampled with uv_MainTex; _FadeTex uses uv_FadeTex. + - Includes "Dither Functions.cginc" and Volumetric.cginc (VolumeSampleColorSimple at vertex, ambientColor into Emission * 10). Sampler use inside Volumetric.cginc not inspected. + - Dither-clip fade via screenPos; multi_compile FLOW_GLOBAL / FLOW_SLOPE / NOISE_SLOPE; per-instance _Fade via UNITY_INSTANCING_BUFFER. Metallic = _Metallic * m.r, Smoothness = _Glossiness * m.a (packed metallic-smoothness). Normal = UnpackNormal(...) * _BumpScale. + - Input struct has no viewDir, worldPos, worldNormal or uv2; only uv_MainTex, uv_FadeTex, screenPos, ambientColor. +- docs/aetheria-release-map.md:358 lists `hull-decals` as a full-game follow-up ("Insignia and damage decals"); :1651 "With a mask texture, painted-in logos can live in the mask; only separate insignia and damage decals remain"; :2140 names `hull-damage-wear` as "the first real Ship parameter". No decal code or shader on origin/master (git grep decal over cs/shader/md hits only that doc). +- Assets/Materials/PBR holds 12 .mat folders (e.g. Carbon_Fibre, Gunmetal_Dark) each with Color, MetallicSmoothness, Normal(GL), Roughness maps. Shader used by those .mat files not determined (LFS pointer parse failed). +- Unity 6.5/6.6 manual banner (FETCHED, docs 6000.6 decals-birp page): Built-In RP "is deprecated and will be made obsolete in a future release", supported incl. bug fixes through the 6.7 LTS lifecycle. That page's only BiRP decal technique is the legacy Projector component. SEARCH: 6.3 LTS docs had no such banner. +- Sampler limits (SEARCH, Unity forum/issue tracker): D3D11 allows 16 active texture samplers per shader (issue tracker "maximum ps_4_0 sampler register index 16 exceeded"); surface shaders spend some internally (lightmaps, shadows); sampling the same texture twice reuses its sampler; a later Unity fix shares some internal samplers inline. No official Unity page stating the number found. Forum mentions GLES 3.0 as the min spec when more are needed. +- Unity Standard shader (FETCHED, StandardShaderMaterialParameterDetail): Secondary Maps = second Albedo and second Normal, usually tiled finer; Detail Mask limits where; "The Secondary normal map channel is more expensive than the primary one". Page does not name the UV set. SEARCH (old forum thread): only detail albedo and normal can use the secondary UV; occlusion cannot. +- Unity built-in has no decal system beyond Projector (FETCHED above). Deferred decals sample = Unity's "RenderingCommandBuffers" project (SEARCH, Unity manual GraphicsCommandBuffers older versions): "each decal is implemented as a box ... affects any geometry inside the box volume", "very similar to how lights are done in deferred shading, except instead of accumulating the lighting we modify the G-buffer textures". Command buffers attach via Camera.AddCommandBuffer at a CameraEvent; in deferred, depth is resolved at AfterGBuffer and shadows at BeforeLighting (SEARCH). The blog post body could not be fetched (404). + +## 1. DECALmachine export (machin3.io/DECALmachine/docs/...) +- Export intro (FETCHED, export_introduction): "DECALmachine 2.0 supports 3 approaches for exporting to Unity, Unreal Engine, etc.": Atlasing, Trim Sheets, Baking. +- Main page (FETCHED, machin3.io/DECALmachine): Atlases "for deferred rendering engines such as Unity HDRP and Unreal Engine"; Trim Sheets "anywhere, including forward renderers like Unity URP"; Baking = decals baked into the parent's UV space. +- Atlas (FETCHED, export_atlas): + - Output: texture set copied to a target folder plus `data.json` (each decal's position in the sheet). Atlas and trim sheet share the data format, so atlased decals appear as "Trims" in the file. Atlased decals get UVs remapped to atlas position; reversible. Channel packing is in the Atlas section of the Decal Export panel. Decal meshes export through the same panel or Blender's native exporter. + - Texture types named: Height, Opacity/masks (example packing: R = diffuse+emissive mask, G = normal mask, B = metallic+roughness mask, from Subset Occlusion / Alpha dropdowns), Base Color, Roughness/Metallic, Normal. + - Unity HDRP: deferred decals blending with the material underneath; Shader Graph decal shaders (Simple/Subset/Panel and Info) on GitHub; Amplify Shader Editor can build equivalents "and add parallax that matches Blender". Unity parallax: possible via ASE using the height map. + - Unity URP: forward, "decals are always opaque", no deferred projectors, trim sheets suggested; "a simple Lit shader with optional alpha mapping is possible, but matching materials is manual"; URP cannot reproduce HDRP material blending. + - Unreal 4.24: mesh decals (not decal actors) for performance; decal-domain material combining base colour/metal/rough/normal and the opacity map; per-channel opacity needs an engine patch; height map into a POM node on the UVs, Bump Offset as cheaper alternative. That section is by a third party. + - Caveats stated: no colour map is produced for normal-mapped decals or atlases (fake AO by blackening normal-only regions in base colour); UE 4.24 mesh decals lack AO. +- Trim sheet export (FETCHED, export_trim_sheet): models with trim detail export via native Blender export; textures via the Atlas export tools after adding one Trim Decal from the sheet and setting the sheet as its atlas; textures can be channel packed. No second-UV or parent-material description on the page. +- Bake (FETCHED, export_bake): "bakes decal textures onto the UV space of their parent objects"; works on objects with decals and at least one UV channel; one or several objects. Fetched page has an empty "Using Bake" heading and no texture list. The caveat "There is an open Blender issue/bug, that limits the usefulness of Baking Decals right now" is SEARCH (search summary of that page and the Superhive, Blender Market, Bazaar listings, which mark baking not recommended). The issue is not named; no source found saying it is fixed in 2.17. Listing: v2.17 supports Blender 4.3 to 5.2 (SEARCH). +- Material matching (FETCHED, match_material, shortcut DV): Simple, Subset, Panel decals, not Info. Source material must end in a Principled BSDF; nodes feeding it are ignored, "simple, parameter-based materials work best". Up to three material components per decal; Re-Apply and asset loaders match only Material and Material 2. Trim sheet materials matchable only if the sheet has no Color, Metallic or Roughness map. Blender-side only. +- Transfer UVs (FETCHED, transfer_uvs): creates a second UV channel on the decal mesh, copies the parent's UVs into it, copies the parent's image textures into the decal material (only UV-mapped image textures wired directly into a Principled BSDF; generated-coordinate parents use Init Generated Coords). Can use the parent's normal map and apply parallax to the transferred textures; works properly "only when the decal's and parent's UV spaces face the same way", which the page says is rarely the case. Page does not say whether this second UV channel is meant for export. +- Parallax (FETCHED, docs/assets): Simple, Subset, Panel decals (Panel inconsistent on the page) use parallax to offset UVs; view-angle dependent; depth into the surface works better than above; one depth level better than several; square decals better than elongated; mirrored decals need flipped UVs; Eevee flicker when a decal sits too close to the surface (fix: Displace modifier height or viewport clip start). Page does not name the height map or its channel. +- Decal types (FETCHED): Simple = normal + parallax, one material component; Subset = normal + parallax, two; Panel = normal, up to three; Info = colour. +- Not found: any documented export path of decal height into a forward hull shader; the atlas docs' only forward answer is URP trim sheets with manual material matching. + +## 2. Shipped-game practice +- Star Citizen (SEARCH, Polycount/Unreal forum threads; no primary talk found): community accounts of floating mesh decals blended over the base material; a secondary blend shader driven by vertex alpha for subtle wear in occluded areas (attributed to an SC developer post); POM on decals is hearsay; decals need an offset to avoid z-fighting; ships too large to bake from one high-poly. Whitepaper link on the Polycount thread not retrieved (polycount.com/discussion/comment/2585025 returned 403). Treat as unconfirmed. +- Unreal mesh decals (SEARCH, forum): POM works on decal actors but not mesh decals per one report; opacity tied to all channels so normal maps get affected; metalness/AO hard on mesh decals. +- Doom 2016 (SEARCH, DF interview, 80.lv, id Tech 6 wiki): opaque geometry and decals use clustered-forward shading; frustum grid of lights, probes and decals looked up in the forward pixel shader; 4000-decal in-view limit; depth prepass + equal test; decals are projected volumes in the cluster structure, not mesh decals. Doom Eternal course notes (advances.realtimerendering.com/s2020/RenderingDoomEternal.pdf, listed, not opened): fully forward, hybrid cluster+tile lists. +- Godot decal docs (SEARCH): a normal-only decal uses the albedo alpha to decide where the surface normal is overridden. Bevy clustered decals (SEARCH, commit): normal maps combined with Whiteout blending (SIGGRAPH 2007), chosen because it is commutative and associative unlike RNM. Both are projected decals, not mesh decals. +- Trim sheets vs decal sheets (SEARCH, Polycount forum answer): trim sheet = textures side by side in one bitmap; decal sheet = same layout overlaid on another texture via alpha edges or buffered rendering (deferred/stencil). Trim sheets suit repeated machinery/pipes. +- Insomniac/Ratchet trim-sheet talks, Halo, Destiny, Titanfall mesh-decal talks: not found (searches returned a members-only GDC lighting talk and a fan "ultimate trim" article on 80.lv). Polycount wiki decals / trim sheet pages: not retrieved. Destiny/Halo/Titanfall technique details: KNOWLEDGE only, unverified, not stated here. + +## 3. Parallax techniques +- SEARCH (Wikipedia, Tatarchuk SI3D06 PDF advances.realtimerendering.com/s2006/Tatarchuk-POM.pdf, listed not opened): POM first published 2005 (Brawley and Tatarchuk, ShaderX3); ray-marches a heightfield per fragment; "substantial GPU time", fragment-bound; changes apparent surface not the silhouette, so a flat outline shows at glancing angles; earlier methods aliased at grazing angles, Tatarchuk's adds adaptive sampling and refined intersection; prism POM (Dachsbacher and Tatarchuk 2007) addresses silhouettes; sensitive to fragment-vs-face normal variation; multi-pass use degrades from height mismatch. Step counts not surfaced. +- KNOWLEDGE (unverified): simple parallax mapping with offset limiting costs one extra height sample and a view-vector offset; steep/relief/POM variants take N linear steps (commonly 8-32) plus refinement; all need a tangent-space view vector; offset UVs can leave a decal's alpha/UV island at its edge. +- Alternatives named in sources: normal-map-only lines (Godot normal-only decal; DECALmachine Panel/Simple decals are normal-mapped); Unreal Bump Offset as cheaper than POM (FETCHED, DECALmachine atlas page); real geometry insets ("limited use of actual geometry" for silhouettes, SEARCH). + +## 4. Layering tiling material + global wear masks in built-in +- Unity Standard detail maps and detail mask: see section 5. +- Surface-shader extension pattern: GlowFade (PROJECT) shows surf function, extra samplers, Input struct fields. KNOWLEDGE (unverified): `uv2_Name` in Input reads UV1; `viewDir` and `worldPos` are available Input members. +- Height-blended material layers, per-ship curvature/AO mask packed RGB on UV1: no source consulted this session. KNOWLEDGE (unverified): mask-driven lerp of albedo/smoothness/normal with optional height-based contrast; mask on a unique non-overlapping UV channel because tiling UV0 repeats. +- Hand-retopologised hulls with deliberate seams: no source consulted on seam handling for tiling materials or triplanar alternatives. + +## Gaps for Self +- Which camera runs deferred in ARPG.unity:30582 and whether gameplay uses it. +- Shader assigned to the 12 PBR .mat files; sampler use in Volumetric.cginc. +- Primary sources not opened: Tatarchuk PDF, Doom Eternal PDF, Unity RenderingCommandBuffers project, SC whitepaper, identity of the DECALmachine Blender bug. From 8755da5ecb5f08f201a4c8c388f5e1bdb17c1e1c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 05:12:27 +0200 Subject: [PATCH 38/49] Release map: articulated mounts re-mapped to the hardpoint-arc ruling Imagination r2 re-map (session self-2026-10-10-ag): the sim already carries per-hardpoint arc and weapon traverse; links are rig only. Target doc moves articulated weapon mounts into scope under ruling mount-arc-gun-traverse. Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 253 +++++++++++++++++++++++--------- docs/aetheria-release-target.md | 4 +- 2 files changed, 183 insertions(+), 74 deletions(-) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index fe79044d6..08528d188 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -7332,14 +7332,19 @@ ships-authoring-tool r2, ships-player r3. New: cut player-build r1, follow-up ## Articulated mounts Mapped 2026-10-09 by Imagination (`imagination-aeth-articulated`, session `self-2026-10-09-ag`) for follow-up -`articulated-mounts`, against `origin/master` `6d83597b`. Rulings: `arc-and-traverse-on-the-link`, -`rig-is-presentation`, `sim-hooks-presentation-agnostic`, `projectile-flight-is-presentation`, -`ship-roles-are-tags`, `thrusters-radiators-are-meshes`; follow-up `articulated-mount-traverse-min`. -Anchors are exact at `6d83597b`. +`articulated-mounts`, against `origin/master` `6d83597b`. Re-mapped 2026-10-10 by Imagination +(`imagination-aeth-articulated-r2`, session `self-2026-10-10-ag`) against `origin/master` `acea1aa7`, after the +operator's rulings `mount-arc-gun-traverse` (arc on the hardpoint, traverse on the weapon, the link presentation +only; it supersedes `arc-on-the-hardpoint`, which superseded `arc-and-traverse-on-the-link`) and +`solver-in-gamecult-animation`. Rulings in force: `mount-arc-gun-traverse`, `rig-is-presentation`, +`solver-in-gamecult-animation`, `sim-hooks-presentation-agnostic`, `projectile-flight-is-presentation`, +`ship-roles-are-tags`, `thrusters-radiators-are-meshes`. AM1-AM11 were probed at `6d83597b`; AM12-AM17 at +`acea1aa7`. Every anchor in the specs is exact at `acea1aa7`. Where AM1-AM11 speak of links, read them as the +2026-10-09 model; AM12-AM17 and everything after them are the live model. **Scope tension for Self.** Target r4's `not_in_scope` lists "articulated mounts". The operator asked for this work -now (her hermit-crab hull carries its big gun on a jointed arm). The cuts are admitted; the target needs a revision -that takes articulated mounts out of `not_in_scope` before Hands is briefed. +(her hermit-crab hull carries its big gun on a jointed arm). The target needs a revision that takes articulated +mounts out of `not_in_scope` before Hands is briefed. ### Body facts @@ -7427,92 +7432,194 @@ form of CultMath's existing `BoundedLeastSquares`. Every engine Eyes checked (Un Unreal Control Rig) keeps IK in the animation layer; none was found in a math library. Aim-an-axis leaves twist about the aim free, so a chain aims with two effective degrees of freedom. +**AM12. The sim already matches ruling `mount-arc-gun-traverse`; no sim cut is owed.** Probed at `acea1aa7`. +Arc: `HardpointData.FiringArc` (`ItemData.cs:567-569`, key 6) is the per-hardpoint, per-hull arc, and +`FireControl.ArcFor` (`FireControl.cs:34-41`) reads it, falling back to `GameplaySettings.FiringArc` (120) at 0. +Traverse: `WeaponData.Tracking` (`Behaviors/Weapon.cs:70-73`, key 33, degrees per second, +infinity when +unauthored; ruling `weapon-tracking-authored`) is a weapon stat, and `FireControl.TrackingRate` +(`FireControl.cs:239-245`) is that rate times the ship's targeting gear. Nothing reads a link, and no `LinkData`, +`HullLinks` or `HardpointData.Link` exists (`git grep` over Assets, tests and tools: 0). The only stale text is the +comment at `FireControl.cs:133-134`, which says traverse belongs to the link and cites the superseded ruling. +`ShipAuthoring.cs`, `ShipModCatalog.cs`, `ShipModPlan.cs`, `ItemData.cs`, `ShipModVisual.cs`, `ShipModShips.cs`, +`EntityInstance.cs`, `CultCacheDrawers.cs`, `tools/blender`, `docs/moddable-ship-authoring.md` and the ship tests +are byte-identical between `6d83597b` and `acea1aa7` (`git diff --stat`); `FireControl.cs` gained 9 lines, all +below line 755. + +**AM13. Shipped traverse falls with calibre already.** Probe `probe_track.py` (scratchpad +`imagination-aeth-articulated-r2`, cultcache-py over `GameData/Aetheria.cc` at `acea1aa7`), `WeaponData.Tracking` +per weapon item: Small guns 1.5 to 40 degrees per second (ClearPath 12, Earp 8, Spectra 10, ColdFire 6, the two +point-defence guns 40, RainbowLite 1.5); Medium guns 3 to 20 (Autocannon 4, ChargeBlast 3 and 3.5, Flak 20); the +one Large gun (plight) 2; launchers and the mine launcher +infinity. Twenty-two weapon items in all. + +**AM14. Shipped hardpoints and arcs.** Probe `probe_arcs.py` (same scratchpad): the catalog holds four hulls. Only +the Turret hull authors an arc: its two Ballistic hardpoints, 8 cells each (2x4, the ExtraLarge band), at 360. Ship +hulls carry weapon hardpoints of 2 cells (Energy, Ballistic) and 3 or 6 cells (Launcher), all at `FiringArc` 0, so +the default 120 applies. Cut `caliber-bands` (unlanded) states the bands Small 1-2 cells, Medium 3-4, Large 5-6, +ExtraLarge 7-9 only in its test (AM9). + +**AM15. What CultMath has for the rig today.** `F:\Projects\CultLib\packages\cultmath\src\CultMath`, read +2026-10-10: `quaternion` (`quaternion.cs`) is a plain struct with `identity`, `LookRotation(forward, up)` and float4 +conversions; `math.normalize(quaternion)` (`math.cs:225-235`) is the only quaternion operation. There is no +quaternion product, conjugate, axis-angle constructor, vector rotation by a quaternion, slerp, swing-twist split or +joint clamp. `BoundedLeastSquares.Solve(m, n, a, b, lo, hi, x, workspace, out iterations, ...)` minimises +`||A x - b||^2` in a box, dense and allocation-free, with `WorkspaceLength(n)`. That is the solve AM11 names. + +**AM16. A moving part needs no record of its own.** The joint tree (AM6: bones export as ordinary nodes with their +data-bone extras) already says which moving part a weapon mount rides: the nearest joint above the mount's node. +A tag on that bone adds nothing the hierarchy does not say, and the arc and traverse the link record carried now +live on the hardpoint and the weapon (AM12). + +**AM17. The add-on chain under these cuts is unlanded.** Specs `ships-addon-frame` r5, `ships-addon-mounts` r5 and +`ships-addon-mesh-mounts` r1 have no report; `tools/blender` at `acea1aa7` is the pre-r5 add-on (`_save_layout` +absent; Save Layout writes `FiringArc` from the panel's `firing_arc`, `__init__.py:43`, `:111`, `:174`, `:458`). +The add-on specs below are anchored at `acea1aa7` and name the region to re-find once that chain merges. + ### Model page | Kind | Identity | Lifecycle | Authority | |---|---|---|---| -| Link (`LinkData` in `HullData.Links`, key 33) | `Id`: the `aetheria.id` that Tag Link writes on the data bone or object (ruling `ship-roles-are-tags`). Renaming never changes it. For a shipped hull, the id the migration writes. | Edited in Blender's layout buffer and written by Save or Package. A package edited after a save changes the arc or traverse a run's guns use on Continue. No save names a link, because the sim holds no slew state (AM2). If a link is removed while a hardpoint names it, the judge fails the package, compose excludes it, and Continue refuses the run, naming the missing design (`RunSave.RequireDesigns`). It adds nothing to `mod-hull-changed-under-run`, because a link change moves no cell. | The author, through the add-on, for arc and traverse. `HullLinks.Validate` judges it. `FireControl` reads it, only through `ArcFor` and `TrackingRate`. | -| Hardpoint on a link (`HardpointData.Link`, key 7) | The hardpoint's mount id (`Transform`) plus the link id. Empty means a fixed mount: the default arc and the gun's own traverse. | Derived at Package from the mount object's ancestry (the nearest ancestor tagged link, through a bone parent), as mounted `Position` is derived from the object. A mount re-parented in Blender changes link at the next Package. For `ambiguous-hardpoint-origin`, a link is one more way two hardpoints at one origin differ (arc, as rotation already does), so it neither fixes nor worsens that defect. The fix there is to save the hardpoint's identity: C4's element id on `HardpointData`. | The mount's place in the Blender hierarchy; Package derives it. The panel and Studio show it read-only for a mounted row. The judge refuses a name that is not a link of the hull. | -| Rig chain (`ShipJoint` in `ShipAuthoring.Joints`, key 5, and the `link` anchors) | Each joint's `Id` is its bone's `aetheria.id`, assigned once by Package (`joint.`) and kept on the data bone. A link anchor's id is its link's id. | Re-rigging changes only the visual record, and every Package and compose judges it again. No save, sim fact or catalog record names a joint. The Unity rig components are cache-only and rebuilt each boot with the prototype. | The author's Blender bone IK settings (lock, limit, min, max), copied by Package. The judge checks the tree and that the yaw reach covers the link's arc. Unity reads and drives it. No joint feeds a sim fact (ruling `rig-is-presentation`). | +| Hardpoint arc (`HardpointData.FiringArc`, key 6, unchanged) | The hardpoint's mount id (`Transform`) on its hull. 0 is a fixed mount at `GameplaySettings.FiringArc`. | Authored per hull in the add-on's panel (or Studio for a shipped hull) and written by Save or Package. A package edited after a save changes the arc a run's guns use on Continue; it moves no cell, so it adds nothing to `mod-hull-changed-under-run`. | The ship author. `ShipAuthoringStore.Validate` judges it finite and non-negative and, for a mount on a joint, positive and within the joint chain's yaw reach. `FireControl.ArcFor` is its one reader. | +| Traverse (`WeaponData.Tracking`, key 33, unchanged) | A stat of the weapon item. | Authored per weapon design; any gun goes on any mount (ruling `mount-arc-gun-traverse`). | The weapon designer. `FireControl.TrackingRate` prices it; the rig presenter slews the picture at the same rate. | +| Joint (`ShipJoint` in `ShipAuthoring.Joints`, key 5) | Its bone's `aetheria.id`, which is its GLB node id. Package assigns `joint.` once to a data bone on a weapon mount's chain and keeps it there. | Re-rigging changes only the visual record; every Package and compose judges it again. No save, sim fact or catalog record names a joint. | The author's Blender bone IK settings (lock, limit, min, max), copied by Package. Validate judges the tree and the reach; Bind judges it against the GLB. No joint feeds a sim fact (ruling `rig-is-presentation`). | +| A mount's joint (`ShipAnchor.Joint`, key 5) | On a `weapon-mount` anchor only: the nearest joint above the mount's node; empty for a fixed mount. | Derived at Package from the mount's bone ancestry, as mounted `Position` is derived from the object. Re-parenting in Blender changes it at the next Package. | The Blender hierarchy; Package derives it; Bind refuses a GLB whose hierarchy disagrees. No panel or Studio edit writes it. | +| Rig (the Unity `MountRig`, one per driven joint) | The driven joint's id. | Cache-only: built with the prototype at boot from `ShipModPlan.Rigs` and the rest pose. | Writes only its joints' Transforms. Reads the lead weapon's `GunSolution.Direction` and `TrackingRate`. | -**C4 (follow-up `c4-ship-element-ids`).** Links are a fourth element kind for variants: `LinkData` rows keyed by -`Id`, as hardpoints are keyed by `Transform`. A heavy variant that narrows a link's arc is a tail-slot patch by link -id. `ship_cc` carries link rows' unknown slots by id, as it does for hardpoints and anchors; `ships-addon-links` -lands that. +A driven joint is a joint some weapon-mount anchor names. Its chain is that joint and its ancestors up to, not +including, the nearest ancestor that is itself driven, so a turret on a turret solves as two chains and the outer +chain never moves under the inner gun's solve. + +**C4 (follow-up `c4-ship-element-ids`).** No link element kind exists for variants: a variant that narrows a gun's +arc is a hardpoint tail-slot patch on `FiringArc`, by mount id, which C4 already plans. ### Authority map -- **Owner.** `LinkData`, authored per hull, owns arc width and traverse speed. The Blender hierarchy owns whether a - hardpoint is on a link, derived into `HardpointData.Link` at Package. The Blender bone IK settings own the joint - limits, packaged into `ShipAuthoring.Joints`. -- **Sim inputs.** `HullData.Links` and `HardpointData.Link`, nothing else. -- **Outputs.** `ArcFor`, `InArc`, `AimDirection` and `TrackingRate`, signatures unchanged. Presentation reads - `GunSolution.Direction` of a link's lead weapon and the link's traverse. -- **Derived.** `HardpointData.Link`, from the hierarchy. A fixed mount's arc, which is `GameplaySettings.FiringArc`. - An object link's joint (one yaw degree of freedom, plus or minus half the arc), at Package. The Unity link rigs - (cache-only). -- **Demoted, then cut.** `HardpointData.FiringArc` is no longer an owner; the link is. `links-record` refuses a - hardpoint that names a link while its `FiringArc` is set. `links-arc` stops reading it and refuses it on packages. - `retire-firing-arc` deletes it and retires key 6. `link-rig-record` replaces the `articulation` role with `link`. -- **Forbidden writers.** No joint limit, bone length, IK result or Unity transform feeds `ArcFor` or - `TrackingRate`. No panel or Studio edit writes a mounted row's `Link`. No helper invents an arc or a traverse. +- **Owner.** The hardpoint owns its arc (`FiringArc`), per hull. The weapon owns its traverse (`Tracking`). The + Blender hierarchy owns which joint a mount rides, derived into `ShipAnchor.Joint` at Package. The bone IK settings + own the joint limits, packaged into `ShipAuthoring.Joints`. FireControl owns the aim (`GunSolution.Direction`). +- **Sim inputs.** `HardpointData.FiringArc` and `WeaponData.Tracking`, as on `acea1aa7`. Nothing the rig adds. +- **Outputs.** `ArcFor`, `InArc`, `AimDirection`, `Solution` and `TrackingRate`, unchanged. Presentation reads the + lead weapon's `GunSolution.Direction` and `TrackingRate`. +- **Derived.** `ShipAnchor.Joint`, from the hierarchy. A driven joint's chain, from the joint tree. Yaw reach, from + the chain's degrees of freedom. The rigs and their `AimChain`s (cache-only). +- **No longer owners.** The link is not a record: `LinkData`, `HullData.Links`, `HardpointData.Link`, the Tag Link + operator and link rows are not built (cuts `links-record`, `links-arc`, `ships-addon-link-rows` withdrawn). + `FiringArc` is not retired (cut `retire-firing-arc` withdrawn). The `articulation` role is cut; a joint is not an + anchor. +- **Forbidden writers.** No joint limit, bone length, IK result or Unity transform feeds `ArcFor`, `TrackingRate` + or any sim fact. No panel or Studio edit writes `ShipAnchor.Joint`. No helper invents an arc. The add-on keeps no + copy of `GameplaySettings.FiringArc`. +- **Shared paths.** Validate's reach check and `ShipModPlan.Rigs` define a chain through one helper. Package and + the gizmo drag derive a mount's place through the same hull_grid functions. ### Rationale -**Links are flat in `HullData`; chains live in the visual record.** The sim reads only the link a hardpoint names -(ruling `rig-is-presentation`). A turret on a turret is a chain of joints, and a chain is presentation, so `LinkData` -has no parent: its arc is the whole coverage the author grants that part. Parenting lives in `ShipJoint.Parent`. A -link's chain runs from its joint up to the nearest ancestor that is a link, or to the root, and parent links solve -first. +**A moving part is a joint, not a record.** The operator ruled the link carries no sim stat (AM12). What remained +of it was a tag and a row holding nothing but an id. The joint tree already says which part a mount rides (AM16), +so the record keeps only what the presenter and the judge need: the joints with their limits, and on each weapon +mount the joint it rides. Moving parts come from armature bones only. A turret built from plain objects is a +one-bone armature; there is no object-link path, so joints have one source. -**One facing per link.** Weapon hardpoints on one link must share a `Rotation`. `MountDirection` stays derived from -the item's rotation (`FireControl.cs:28-33`), the link needs no facing field, and the presenter's lead weapon aims -for every gun on the part. +**A bone with no free axis is not a joint.** Package turns a chain bone into a joint only when its IK settings +leave at least one degree of freedom. A gun on a fully locked bone has no joint above it and is a fixed mount at +its own arc, the default included. -**Why `FiringArc` is retired rather than kept for fixed mounts.** Two places that state an arc are two owners. A -fixed mount is the degenerate case: no link, the default arc, the gun's own traverse. Any other arc is a link, -including the shipped Turret hull's, which migrates to one link with arc 360 and traverse +infinity, so its behaviour -is unchanged. The subtraction is its own cut and lands after the add-on cuts that edit the same panel, so no open -cut's hunks are stranded. +**A gun on a joint states its arc.** The judge checks the chain's yaw reach against the arc, and the judge has no +`GameplaySettings` (the default is a game setting, and the add-on keeps no copy of it either). So a weapon whose +mount rides a joint must author `FiringArc` above 0. Weapons on one joint share one `Rotation` and one `FiringArc`, +because the lead weapon's aim moves them all. -**Traverse +infinity means no mechanical limit.** It matches `Weapon.Tracking`'s convention ("an unauthored gun is -+infinity", `FireControl.cs:238-245`), so `min` needs no special case. The add-on shows it as a Free traverse -toggle; zero and NaN are refused. +**The reach check is yaw only, and conservative** (unchanged from 2026-10-09). The sim is planar. A chain covers the +arc when the summed ranges of its degrees of freedom whose axis lies within 5 degrees of Ship Root up reach the +arc, capped at 360. Serial parallel axes add, so the sum is exact for the arms the add-on makes. A chain whose yaw +comes only from tilted axes is refused with a message naming the joint. -**Joint limits come from Blender's bone IK settings, not from Limit Rotation constraints.** Blender's own IK solver -honours `lock_ik_*`, `use_ik_limit_*` and `ik_min_*`/`ik_max_*`, so an author previewing with an IK constraint sees -the limits the game uses. The semantics follow Blender's: an axis is free unless locked or limited. Package writes -each degree of freedom as a unit axis in Ship Root space at rest, converted to glTF axes, with a min and max in -degrees. This keeps the record independent of the exporter's bone-axis correction, which this map did not verify. +**Joint limits come from Blender's bone IK settings, not from Limit Rotation constraints** (unchanged). Blender's +own IK honours `lock_ik_*`, `use_ik_limit_*` and `ik_min_*`/`ik_max_*`; glTF carries no constraint (AM6). -**The reach check is yaw only, and conservative.** The sim is planar. A link's chain covers its arc when the summed -ranges of the chain's degrees of freedom whose axis lies within 5 degrees of Ship Root up reach the arc (an arc of -360 needs a summed 360). Serial joints about parallel axes add, so the sum is exact for the arms the add-on makes. A -chain whose yaw comes only from tilted axes is refused with a message naming the link. That is the price of a check -the judge can state in one line. +**The presenter slews at the weapon's traverse.** It turns its aim toward the lead weapon's +`GunSolution.Direction` (already arc-clamped by the sim) at most `FireControl.TrackingRate(lead, entity)` degrees +per second, the rate the sim prices the angular term with. +infinity is no slew. The sim stays stateless. -**The solver is behind a port.** Aetheria's presenter needs only this: a chain (joints root-first, their rest -offsets, their degrees of freedom with limits), an effector (the muzzle offset and its local aim axis), the current -angles and a target direction; it gets back new angles. `link-rig-presenter` codes against that port. Question -`solver-owner` decides who implements it. +**No Aetheria-side solver port.** Ruling `solver-in-gamecult-animation` puts the solve in a pure CultLib package. +The presenter calls that package's API; an Aetheria interface with one implementation would be a second surface +buying nothing a pure, headless-testable library does not already give. -**Thrusters on links stay a follow-up.** A gimballed thruster changes where thrust points. That is a sim fact, and -the thrust allocator (cuts `thrust-allocator-core`, `thrust-allocator` and `envelope-from-columns`, all in flight) -reads thrusters as fixed columns. Putting thrusters on links now would reopen those cuts. The judge refuses a -non-weapon hardpoint on a link and names follow-up `gimballed-thrusters`. +**Calibre and arc.** The operator's own examples put a heavy gun on a wide arc (the crab's claw) and a light gun in +a narrow port, and her ruling says calibre caps are enforced "if at all". The shipped Turret's ExtraLarge +hardpoints sweep 360 (AM14), and the weapon's traverse already falls with calibre (AM13), which prices a heavy gun +on a wide arc through `TrackingRate`. Question `hardpoint-arc-caps` puts the choice to her with no-cap recommended. +Under no-cap, follow-up `caliber-bands-one-table` is moot: the band table stays in `caliber-bands`' test, its only +reader. Under either cap option the judge reads the bands, so they become one table in ServerShared and that test +reads it. -**Cut order.** `links-record` (types and judge), then `links-arc` (FireControl reads the link; the Turret hull and -the test fixtures migrate), carry the sim. `link-rig-record` adds the joint record and the GLB checks. -`ships-addon-links` lands the Blender side after `ships-addon-mounts`. Mounts r5 needs no revision: it writes no -arc, and `ships-addon-links` extends its derivation. `ships-addon-gizmos` r4 replaces r3. `link-rig-presenter` -waits on `solver-owner`, `link-calibre-judge` waits on `calibre-arc-caps`, and `retire-firing-arc` goes last. +**Thrusters on joints stay a follow-up.** A gimballed thruster changes where thrust points, which is a sim fact the +thrust allocator does not model. Validate refuses `ShipAnchor.Joint` on any non-weapon anchor and Bind refuses a +thruster or radiator node under a joint, both naming follow-up `gimballed-thrusters`. + +### The rig's interface to GameCult.Animation and CultMath + +This is what `link-rig-presenter` calls; a later cultlib-gaps pass maps the cuts that provide it. Types and +operations only. Angles are radians at this API (the record's degrees convert once, when the presenter builds its +chain). + +CultMath (`quaternion`, `float3`; pure and allocation-free): +- `math.mul(quaternion a, quaternion b) -> quaternion`, composing in the Unity.Mathematics convention (`mul(a, b)` + applies `b` first). +- `math.rotate(quaternion q, float3 v) -> float3`. +- `math.conjugate(quaternion q) -> quaternion` and `math.inverse(quaternion q) -> quaternion`. +- `quaternion.AxisAngle(float3 unitAxis, float radians) -> quaternion`. +- `math.slerp(quaternion a, quaternion b, float t) -> quaternion` (shortest arc). +- Swing-twist and joint clamps, which the ruling also gives CultMath. The presenter does not call them (its degrees + of freedom are per-axis boxes); GameCult.Animation may: `math.swing_twist(quaternion q, float3 unitTwistAxis, out + quaternion swing, out quaternion twist)`, `math.clamp_hinge(quaternion q, float3 unitAxis, float min, float max) + -> quaternion`, `math.clamp_cone(quaternion swing, float3 unitAxis, float maxAngle) -> quaternion`, + `math.clamp_twist(quaternion twist, float3 unitAxis, float min, float max) -> quaternion`. + +GameCult.Animation (a new CultLib package, netstandard2.1, depending only on CultMath, and published as a UPM package +`org.gamecult.animation` beside `org.gamecult.cultmath` so Aetheria's `Packages/manifest.json` pins it the same way): +- `AimJoint { float3 RestPosition; quaternion RestRotation; }`: rest offset and rotation relative to the previous + joint (the first relative to the chain root's frame). +- `AimDof { int Joint; float3 Axis; float Min; float Max; }`: a unit axis in that joint's local frame and its bounds. +- `AimEffector { float3 Offset; float3 Axis; }`: the muzzle's offset and unit aim axis in the last joint's frame. +- `AimChain.Create(ReadOnlySpan joints, ReadOnlySpan dofs, AimEffector effector) -> AimChain`: + validates (finite values, unit axes within a tolerance, `Min <= Max`, `Joint` ascending and in range) and throws + `ArgumentException` naming the field, never the value. Exposes `JointCount` and `DofCount`. +- `AimChain.WorkspaceLength(in AimChain chain) -> int`. +- `AimChainSolver.Solve(in AimChain chain, ReadOnlySpan current, float3 targetDirection, Span next, + Span workspace) -> AimSolveStatus` (`Converged`, `IterationCap`, `InvalidInput`): turns the effector's aim + axis toward `targetDirection` (chain root frame) by damped least squares over `BoundedLeastSquares`, every angle + in `next` inside its `[Min, Max]`, twist about the aim left free; deterministic, allocation-free, warm-started from + `current`. +- `AimChain.Pose(in AimChain chain, ReadOnlySpan angles, Span localRotations)`: each joint's + local rotation, its rest rotation times the product of its degrees of freedom's axis-angle rotations in DOF order. +- `AimChain.EffectorDirection(in AimChain chain, ReadOnlySpan angles) -> float3`: the aim axis in the chain + root's frame (forward kinematics), for tests and the presenter's settled check. + +Aetheria keeps the glTF-to-Unity axis conversion (`JointAxes.ToLocal`, ServerShared, pure), the slew (planar angle +arithmetic over the existing `math.atan2` and `math.rotate(float2, radians)`), and the `UnityEngine.Quaternion` +conversion at the Transform write. + +### Cut order + +1. `link-rig-record` r3, no dependency: the joint record, the judge's rig rules, Bind's GLB checks, + `ShipModPlan.Rigs`, and the stale FireControl comment. **First.** +2. `ships-addon-gizmos` r6 after `ships-addon-mesh-mounts`: arc fans from the row's `FiringArc`. +3. `ships-addon-link-package` r3 after `link-rig-record`, `ships-addon-mounts` and `ships-addon-mesh-mounts`: + Package writes the joints and each weapon mount's joint. +4. `link-rig-presenter` r2 after `link-rig-record`, `ships-addon-link-package`, and the CultLib release carrying the + interface above (cultlib-gaps), pinned. +5. `link-calibre-judge` r2: blocked on question `hardpoint-arc-caps`; withdrawn if she rules no-cap. + +Withdrawn: `links-record` r1, `links-arc` r1, `retire-firing-arc` r1, `ships-addon-link-rows` r1, and question +`calibre-arc-caps`. ### Admitted -Receipts `mind-commit-b0c6b373...`, `mind-commit-c00a16b5...`, `mind-commit-08fce00f...` and `mind-commit-06e09a45...` -(2026-10-09 21:18-21:24 UTC). New cuts: links-record r1, links-arc r1, link-rig-record r2 (r1 superseded for one -out-of-range anchor), ships-addon-link-rows r1, ships-addon-link-package r1, link-rig-presenter r1, link-calibre-judge -r1, retire-firing-arc r1. Revised: ships-addon-gizmos r5 (r3 superseded by r4 and r4 by r5 for an untested negative -pattern). ships-addon-mounts r5 is unchanged. Questions: `solver-owner` (raised in link-rig-presenter) and -`calibre-arc-caps` (raised in link-calibre-judge). Follow-ups: `gimballed-thrusters`, `caliber-bands-one-table`. -context-pack read every anchor SAME at `6d83597b`. +2026-10-09 (`imagination-aeth-articulated`): receipts `mind-commit-b0c6b373...`, `mind-commit-c00a16b5...`, +`mind-commit-08fce00f...`, `mind-commit-06e09a45...`. + +2026-10-10 (`imagination-aeth-articulated-r2`): receipts `mind-commit-0b2e932e...` (withdrew links-record r1, links-arc r1, +retire-firing-arc r1, ships-addon-link-rows r1; link-rig-record r3; ships-addon-gizmos r6), +`mind-commit-0c23fa01...` (ships-addon-link-package r2, link-rig-presenter r2, link-calibre-judge r2, question +`hardpoint-arc-caps`, withdrew question `calibre-arc-caps`) and `mind-commit-40788d58...` (ships-addon-link-package r3, +r2's arm smoke pins fixed). context-pack read every anchor SAME at `acea1aa7`. diff --git a/docs/aetheria-release-target.md b/docs/aetheria-release-target.md index 9f8a6b53a..402a6fe88 100644 --- a/docs/aetheria-release-target.md +++ b/docs/aetheria-release-target.md @@ -146,7 +146,9 @@ The typed target carries these as labelled statements. The reasons: (ruling `targeting-sublinear-wanted-not-critical`), and belt rendering through the index. - The faction-play tender loop (`faction-play-4`), drones and munitions, the - Colosseum, articulated mounts. + Colosseum, gimballed thrusters. Articulated weapon mounts are in scope: the arc + is the hardpoint's, traverse is the weapon's, and the moving part is + presentation only (ruling `mount-arc-gun-traverse`). - Narrative: storylets, Ghostlight, Ink beats per boss. - Volumetrics and the nebula backport. - Asura and procedural planet content. From b623d519f77c30c21c45f7e915c0eaad422b42b2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 05:26:51 +0200 Subject: [PATCH 39/49] ThrottleLock tests: pin the exact tick the lock ends, as a function of Duration Replaces TheLockHoldsThroughMostOfItsDurationAndEndsAfter with a theory over Duration 1 and 2 at 0.25 s ticks. Builds. Co-Authored-By: Claude Opus 5.5 --- .../ThrottleLockTests.cs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs index 032e74d0f..3f6109155 100644 --- a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -246,26 +246,29 @@ public sealed class ThrottleLockTests : IDisposable Assert.Equal(0f, f.Forward.Axis); } - // Finding lock-end-boundary-unpinned. Mutation: the lock ends at half Duration. Stepped in quarter-second - // ticks over a 1 s Duration, the lock holds through 0.75 s and is gone by 2 s. The exact tick of expiry is - // consumable-host's rule and is not pinned here. - [Fact] - public void TheLockHoldsThroughMostOfItsDurationAndEndsAfter() + // Finding lock-end-boundary-unpinned, finding lock-end-at-90pct-unpinned. Mutations: the lock ends at half + // Duration, at 0.8 or 0.9 of Duration, or one tick late. Duration 1 stepped in 0.25 s ticks (exact in + // floats) locks the axes on exactly four ticks (reverse stays 0), the effect is gone when the fourth ends, + // and the fifth tick follows the stick. The count scales with Duration: 2 s locks eight ticks. + [Theory] + [InlineData(1f, 4)] + [InlineData(2f, 8)] + public void TheLockEndsAtTheExactTickOfItsDuration(float duration, int lockedTicks) { - var f = Build(1f); + var f = Build(duration); using var _ = f.Cache; f.ActivateLock(); f.Ship.MovementDirection = float2(0, -1); - for (var i = 0; i < 3; i++) + for (var i = 0; i < lockedTicks; i++) { - f.Ship.Update(.25f); Assert.True(f.Ship.ThrottleLocked); + f.Ship.Update(.25f); + Assert.Equal(0f, f.Reverse.Axis); + Assert.Equal(1f, f.Forward.Axis); } - f.Ship.Update(.25f); - Assert.Equal(0f, f.Reverse.Axis); - for (var i = 0; i < 4; i++) f.Ship.Update(.25f); Assert.False(f.Ship.ThrottleLocked); f.Ship.Update(.25f); Assert.Equal(1f, f.Reverse.Axis); + Assert.Equal(0f, f.Forward.Axis); } } From 11a7843c61856c76cd215a5aa986f70c9793b6d8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 05:30:38 +0200 Subject: [PATCH 40/49] StatResolver tests: boosts on one stat multiply, constants add, gear and consumable share the rule Pins ruling boost-stacking-multiply through the production path (gear and consumable StatModifierData on a ship, read via EquippedItem.Evaluate): TwoScalesOnOneStatMultiply, AConsumableBoostMultipliesWithGear, TwoConstantsOnOneStatAdd. Tests only; no production change. Not yet run; the commit is unverified. Co-Authored-By: Claude Opus 5.5 --- .../StatResolverTests.cs | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/StatResolverTests.cs b/tests/Aetheria.Shared.Tests/StatResolverTests.cs index 0c0283f20..a9ed2e28b 100644 --- a/tests/Aetheria.Shared.Tests/StatResolverTests.cs +++ b/tests/Aetheria.Shared.Tests/StatResolverTests.cs @@ -452,4 +452,125 @@ public sealed class StatResolverTests : IDisposable Assert.Equal(baselineCache, resolver.CacheEntryCount); Assert.Equal(baselineModifiers, resolver.ModifierEntryCount); } + + // Ruling boost-stacking-multiply: every multiplier on one stat multiplies, with no stacking penalty, and every + // constant adds. The rig is the production path end to end: gear carrying StatModifierData is equipped on a ship + // beside a thruster, a consumable is minted through ItemManager.CreateInstance and activated on the ship, and + // the resolved value is read through EquippedItem.Evaluate. Nothing here calls AttachModifier by hand. + private sealed class ThrustRig + { + public CultCache Cache; + public ItemManager Items; + public Ship Ship; + public EquippedItem Engine; + public PerformanceStat Thrust; + public Func Consumable; // null when the rig was built without one + + public float Resolved => Engine.Evaluate(Thrust); + // Two ticks: the first executes every modifier, the second applies it (StatModifier.Update). + public void Tick(float dt) { for (var i = 0; i < 2; i++) Ship.Update(dt); } + } + + private static StatModifierData ThrustModifier(StatModifierType type, float value) => new StatModifierData + { + Stat = new StatReference { Target = nameof(ThrusterData), Stat = nameof(ThrusterData.Thrust) }, + Modifier = new PerformanceStat { Min = value, Max = value }, + Type = type + }; + + // A Thrust-10 thruster and one gear per entry of `gear`, in that attach order, plus (optionally) a consumable + // whose single modifier is `consumable`. The hull has four hardpoints, so at most three gear modifiers. + private ThrustRig BuildThrustRig((StatModifierType Type, float Value)[] gear, (StatModifierType Type, float Value)? consumable = null) + { + var thrust = new PerformanceStat { Min = 10, Max = 10 }; + var cache = AetheriaStores.Open(Catalog, catalogWritable: true); + cache.Upsert(new TestCatalogGlobal { Name = "Temperament" }); + var hullShape = new Shape(3, 3); + foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true; + var cells = new[] { HardpointCell, SecondHardpointCell, FirstGunCell, SecondGunCell }; + cache.Upsert(new HullData + { + Name = "Skiff", HullType = HullType.Ship, Shape = hullShape, Durability = 10, + Hardpoints = cells.Select(c => new HardpointData { Type = HardpointType.Sensors, Position = c, Shape = new Shape() }).ToList() + }); + cache.Upsert(new GearData { Name = "Engine", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 10, Behaviors = { new ThrusterData { Thrust = thrust } } }); + for (var i = 0; i < gear.Length; i++) + cache.Upsert(new GearData { Name = "Boost" + i, Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 10, Behaviors = { ThrustModifier(gear[i].Type, gear[i].Value) } }); + if (consumable != null) + cache.Upsert(new ConsumableItemData { Name = "Stim", Duration = 1f, Shape = new Shape(), Behaviors = { ThrustModifier(consumable.Value.Type, consumable.Value.Value) } }); + cache.FlushAsync().Wait(); + + var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); + var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null); + var ship = new Ship(items, zone, Mint(cache, items, cache.GetByName("Skiff")), new EntitySettings()); + Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Engine")), cells[0])); + for (var i = 0; i < gear.Length; i++) + Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Boost" + i)), cells[i + 1])); + zone.Entities.Add(ship); + ship.Activate(); + + Func mint = null; + if (consumable != null) + { + var design = cache.GetByName("Stim"); + mint = () => Assert.IsType(items.CreateInstance(items.Lots.Add( + new Lot { Design = cache.RefOf(design), Origin = new Attributed(), Quality = .5f, Roles = new List() }))); + } + return new ThrustRig { Cache = cache, Items = items, Ship = ship, Engine = ship.Equipment.Single(e => e.Data.Name == "Engine"), Thrust = thrust, Consumable = mint }; + } + + // Two scales multiply: x1.2 and x1.25 on one stat resolve to base x1.5, and x1.1, x1.2, x1.25 to base x1.65, + // whatever order they attach in. Mutations at StatResolver.ScaleModifier: a sum of excesses reads 14.5 and + // 15.5, the strongest-only rule 12.5, a diminishing curve 14.67, and `result *= value * value` or + // `result *= value + 0.01f` (functions of the scale itself) read off 15. + [Theory] + [InlineData(new[] { 1.2f, 1.25f }, 15f)] + [InlineData(new[] { 1.25f, 1.2f }, 15f)] + [InlineData(new[] { 1.1f, 1.2f, 1.25f }, 16.5f)] + [InlineData(new[] { 1.25f, 1.1f, 1.2f }, 16.5f)] + [InlineData(new[] { 1.2f, 1.25f, 1.1f }, 16.5f)] + public void TwoScalesOnOneStatMultiply(float[] scales, float expected) + { + var rig = BuildThrustRig(scales.Select(s => (StatModifierType.Multiplier, s)).ToArray()); + using var _ = rig.Cache; + Assert.Equal(10f, rig.Resolved, 3); // modifiers not yet applied + rig.Tick(.1f); + Assert.Equal(expected, rig.Resolved, 3); + } + + // A gear modifier and a consumable modifier on one drive's Thrust share one entry and multiply: x1.2 gear and + // x1.5 consumable read x1.8, and when the consumable expires only the gear's x1.2 remains. + [Fact] + public void AConsumableBoostMultipliesWithGear() + { + var rig = BuildThrustRig(new[] { (StatModifierType.Multiplier, 1.2f) }, (StatModifierType.Multiplier, 1.5f)); + using var _ = rig.Cache; + rig.Tick(.1f); + Assert.Equal(12f, rig.Resolved, 3); // gear alone + + rig.Ship.ActivateConsumable(rig.Consumable()); + rig.Tick(.1f); // 0.8s of the 1s remain + Assert.Equal(18f, rig.Resolved, 3); + + rig.Tick(.5f); // the consumable's duration has run out + Assert.Equal(12f, rig.Resolved, 3); + } + + // Constants add, and compose with scales through PerformanceStat.Evaluate: (base x scale) + constant. +3 and +4 + // resolve as a constant of 7; with a x2 scale beside them the value is Evaluate's for scale 2 and constant 7, + // (10 x 2) + 7. Mutations at ConstantModifier: `result = Math.Max(result, value)` reads constant 4; + // `result += value * 2` reads constant 14. + [Theory] + [InlineData(new[] { 3f, 4f }, new float[0], 1f, 7f, 17f)] + [InlineData(new[] { 3f, 4f }, new[] { 2f }, 2f, 7f, 27f)] + public void TwoConstantsOnOneStatAdd(float[] constants, float[] scales, float expectedScale, float expectedConstant, float expected) + { + var rig = BuildThrustRig(constants.Select(c => (StatModifierType.Constant, c)) + .Concat(scales.Select(s => (StatModifierType.Multiplier, s))).ToArray()); + using var _ = rig.Cache; + rig.Tick(.1f); + Assert.Equal(expectedScale, rig.Ship.Resolver.ScaleModifier(rig.Engine, rig.Thrust), 3); + Assert.Equal(expectedConstant, rig.Ship.Resolver.ConstantModifier(rig.Engine, rig.Thrust), 3); + Assert.Equal(expected, rig.Resolved, 3); + } } From 77e441df31e49b36f1fc63ba4f507ca86eaf6353 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 05:32:45 +0200 Subject: [PATCH 41/49] StatResolver tests: keep the boost gear online with the entity's override so the modifiers execute The first commit's gear modifiers never executed (test gear has no heat response, so it read offline). Builds; the first commit's new tests fail without this. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/StatResolverTests.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/StatResolverTests.cs b/tests/Aetheria.Shared.Tests/StatResolverTests.cs index a9ed2e28b..b156412ab 100644 --- a/tests/Aetheria.Shared.Tests/StatResolverTests.cs +++ b/tests/Aetheria.Shared.Tests/StatResolverTests.cs @@ -505,7 +505,14 @@ public sealed class StatResolverTests : IDisposable var ship = new Ship(items, zone, Mint(cache, items, cache.GetByName("Skiff")), new EntitySettings()); Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Engine")), cells[0])); for (var i = 0; i < gear.Length; i++) - Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Boost" + i)), cells[i + 1])); + { + // A test gear has no authored heat response, so its thermal performance reads 0 and it would sit offline; + // the override is the entity's own switch for keeping a part running, not a stand-in for the rule. + var boost = Mint(cache, items, cache.GetByName("Boost" + i)); + boost.OverrideShutdown = true; + Assert.True(ship.TryEquip(boost, cells[i + 1])); + } + ship.OverrideShutdown = true; zone.Entities.Add(ship); ship.Activate(); From 30d027195b345cd36068d90868531a30c804442b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 06:50:05 +0200 Subject: [PATCH 42/49] Research: prior art on thrust allocation with vectored thrusters Serves follow-ups vectored-thruster-rows and gimballed-thrusters: virtual actuators, nonlinear and cascaded allocation, rate and gimbal limits as per-tick bounds, the zero-thrust singularity, games, shared-angle rows. Co-Authored-By: Claude Opus 5.5 --- docs/research/thrust-vectoring-prior-art.md | 131 ++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 docs/research/thrust-vectoring-prior-art.md diff --git a/docs/research/thrust-vectoring-prior-art.md b/docs/research/thrust-vectoring-prior-art.md new file mode 100644 index 000000000..f2df686a4 --- /dev/null +++ b/docs/research/thrust-vectoring-prior-art.md @@ -0,0 +1,131 @@ +# Prior art: control allocation with tilting / gimballed thrusters + +Serves the fork: vectored thruster rows in Aetheria (follow-ups `aetheria-release:follow_up:vectored-thruster-rows`, `aetheria-release:follow_up:gimballed-thrusters`). A thruster row tilts its thrust vector within a gimbal limit at a slew rate; the thrust allocator chooses the tilt; one gimbal per row, not per thruster. + +Eyes pass, 2026-10-10, session self-2026-10-10-ag. Facts with evidence pointers. No recommendation. Anything inferred is marked "(inferred)". Items marked [not retrieved] were seen only through search snippets or citations in other papers, not read in full. + +## 0. The allocator today (the thing the prior art is compared against) + +Source: `Assets/Scripts/ServerShared/ThrustAllocator.cs` at HEAD `1fc98bdc` of branch `eureka/aetheria-release-thrust-allocator-core` (read via `git show`). + +- Input: `ReadOnlySpan columns` (x starboard acceleration, y forward acceleration, z clockwise yaw rate per unit throttle), `float2 move`, `float turn`. Output: one throttle per thruster in [0,1]. +- One `BoundedLeastSquares.Solve` per call over n throttles plus 4 error variables (unwanted/missing translation per half-axis, linear cost). Yaw row weight 1000, translation rows weight 100, ridge 0.1 on each throttle. Variable bounds: throttles in [0,1], errors in [0, 2]. +- Columns are fixed inputs: the allocation matrix is constant with respect to the unknowns, so the problem is linear in the throttles. + +## 1. Question (a): standard formulations for allocation with tilting actuators + +### 1.1 Virtual actuators: force components as the linear unknowns, angle and magnitude recovered afterwards + +- Voliro hexacopter (Kamel, Verling et al., ETH, arXiv 1801.04581, section "D. Control allocation"). The allocation matrix is nonlinear in the tilt angles. The paper states a nonlinear least squares solve was judged too costly on the onboard computer and proposes "a variable transformation": each rotor's force is split into a vertical and a lateral component, F_v,i = c n_i^2 cos(alpha_i) and F_l,i = c n_i^2 sin(alpha_i) (eqs 16-17). The 12-vector of components maps linearly to the wrench by a constant matrix, solved by pseudo-inverse. Recovery: n_i^2 = (1/c) sqrt(F_v,i^2 + F_l,i^2) (eq 22), alpha_i = atan2(F_l,i, F_v,i) (eq 23). The paper reports several hundred Hz on a Pixhawk. +- Same technique in the marine literature, named "extended thrust configuration" (Sørdalen 1997, Control Engineering Practice 5(9):1223-1231, [not retrieved]; described in Mukwege/Nguyen/Garone arXiv 2510.08119 intro and in Maneuvering-based Dynamic Thrust Allocation, arXiv 2507.18309, section 2: "separating the force components and using the rectangular configuration matrix is convenient as this leads to a constant configuration matrix"). In 2507.18309 fixed-direction thrusters stay in polar form (magnitude) while azimuth thrusters use rectangular components, in one allocation. +- Mukwege/Nguyen/Garone (arXiv 2510.08119, sections 2-4) write the whole problem as d = M F with F in R^(3m) (three force components per thruster, M constant 6 x 3m) and treat the orientation as F_j / |F_j|. + +### 1.2 Nonlinear allocation, solved by sequential local linearisation + +- Johansen, Fossen, Berge, "Constrained nonlinear control allocation with singularity avoidance using sequential quadratic programming", IEEE TCST 12(1):211-216, 2004 [not retrieved; abstract and the description in 2510.08119 section 1.1 and 4]. Each sample: a convex QP approximating the nonlinear program; actuator rate and position constraints; a singularity penalty on 1/det(J(alpha) J(alpha)^T) added to the cost. +- Per 2510.08119 section 4, the penalty (i) does not work when thrusters are fewer than the degrees of freedom (det is identically zero), and (ii) gives no systematic behaviour around zero demand d = 0. The same paper names the equality constraint d = J(alpha) T and the penalty as the source of non-convexity. +- Ryll/Cuniato line and the ETH Voliro follow-ups [not retrieved in full]: a search result describes Cuniato et al. (IEEE T-RO) as a differential allocation including servo and propeller dynamics, claimed free of singularities. + +### 1.3 Sequential / cascaded (forces first, then angles) + +- TU Delft thesis on dual-axis tilt-rotor quadplanes (repository.tudelft.nl, search snippet only) reports a nonlinear SQP allocator and states that a cascaded scheme (linear then angular accelerations) was chosen over a combined one because of large roll and pitch changes. [not retrieved in full] +- A 2021 tilt-rotor quadrotor paper (search snippet, not retrieved) splits the effectiveness matrix into two parts (yaw torque and forward velocity first, then all dynamics) and iterates to convergence. +- Voliro's solution (1.1) is itself two-stage: linear solve for components, then closed-form angle recovery. + +### 1.4 Rate and gimbal limits entering the allocation + +- Per-tick bounds from current state and rate (box on the actuator variable): Tiltrotor NMPC (Nguyen/Garone et al., arXiv 2406.06130, section 4.1): "we use the current state of actuators and their rate of change to update u_min and u_max" every sample, and constrain the controller so its virtual command lies in the image h(u) of the admissible actuator box. There the unknowns are thrust and tilt angle themselves (u = thrust_i, tilt_i) with the rate-updated box; the paper's allocator in its experiments is a pseudo-inverse. +- Mukwege et al. 2510.08119 section 4: thrust saturation as F_i magnitude bound, thrust rate as |F_i - F_i0| <= bound reusing the previous solution F_i0, pointing and angular-rate constraints as convex cones C_i with F_i in C_i (conic combination of 3 vectors, or half-space form A F <= b). Resulting problem class: QCQP at most. For real time it states thrust saturation can be rewritten as three box constraints per actuator (F_x, F_y, F_z min/max), "at the cost of extra conservativeness". +- Marine DP (MARIN, search snippet [not retrieved]): physical limits modelled are saturation, azimuth turn rate and RPM change rate, solved with SQP. +- Maneuvering-based Dynamic Thrust Allocation (arXiv 2507.18309, abstract and section 3.4): a nonlinear reference filter driven by a control Lyapunov/barrier function tracks the optimal allocation with rate limitation of the thruster forces; the optimal allocation itself is computed unconstrained in the manifold p + Q*lambda (particular solution plus nullspace). +- Härkegård and active-set methods (Bodson & Frost, NASA, quoted in a search result [not retrieved]): Härkegård's active-set algorithms for constrained least-squares allocation; "Resolving actuator redundancy - optimal control vs. control allocation", Automatica 41(1):137-144, 2005 (bibliographic fact only, [not retrieved]). Dynamic allocation with rate limits as constraint on the increment is a standard formulation of this literature (inferred from the 2406.06130 and 2510.08119 descriptions; the Härkegård paper was not read). +- Voliro paper states the limitation of the linear trick directly (section IV-C text near eq 23): "the tilting angles and the angular velocities are nonlinearly coupled. Therefore, it is difficult to constrain them separately. This means, that this allocation doesn't account for the slower dynamics of the tilting angles." Example given: hovering at 90 deg roll, two rotors on the vertical axis must "constantly rotate 180 back and forth at a high rate"; tilt servos cannot follow, so forces and moments appear in directions not commanded. The authors state this motivated a further step (text beyond the retrieved excerpt). + +## 2. Question (b): virtual-actuator trick inside a least-squares / QP allocator + +### Bound shapes + +- A real tilt limit is a cone (3D) or a wedge (2D) on the force vector: |F_lateral| <= tan(theta_max) * F_vertical, F_vertical >= 0. 2510.08119 section 4 states pointing constraints are "convex cones C_i", yielding at most a QCQP; the linear-programming-friendly approximation is a box on each component (eqs 40-42), explicitly conservative. +- In a 2D plane with a one-sided (non-reversible) thruster, the cone degenerates to two half-spaces on (F_v, F_l) (inferred from the cone statement above; Aetheria's columns are planar and throttles are in [0,1]). Whether `BoundedLeastSquares` accepts general linear inequality constraints or only variable box bounds was not examined by this pass; the allocator file shows only variable bounds [lo, hi]. +- Marine azimuth with a full circle: the Valčić/Prpić-Oršić result is that removing a forbidden sector from the azimuth circle leaves a non-convex feasible region ("Pacman shape") causing large azimuth swings and slower allocation (CROSBI 899099 and related entries, search results only). + +### Conditioning + +- Mukwege et al. 2510.08119 section 1 and 3 state that pseudo-inverse allocations are "sensible to the singularities" and produce "discontinuous actuator orientations references"; because servos have finite turning rate, crossing a singularity leaves "a period of time where the system is not able to fulfill the demands." They note singular configurations are "also usually related to energy-efficient configurations." +- The linear virtual formulation raises the number of unknowns from one scalar per actuator to two or three (Voliro: 12 variables for 6 rotors). Over-actuation (more unknowns than DOF) means the solution is not unique (Voliro section D; 2406.06130 section 1 mention of ill-conditioned effectiveness matrix). +- 2510.08119 adds a rest-configuration nullspace term to the convex cost (q1 b(d) K_b^T F - q2 ||...||^2, eq 38) which they say preserves convexity and keeps a minimum force on each actuator; they present it as the fix for the orientation discontinuity. + +### Warm start + +- No source retrieved states warm-start behaviour of a bounded least-squares/QP solver specifically under virtual-actuator allocation. 2510.08119 section 4 states the rate constraint reuses the previous solution F_i0 as the centre of the rate box (so the previous solution is part of the constraint, not only an initial guess). Voliro: the pseudo-inverse is memoryless. Orbiter forum simplex thread (section 4): solve time 0-15 ms for 22 thrusters, no warm-start detail. + +### Singularity near zero thrust + +- Angle atan2(F_l, F_v) is undefined at F = 0 and discontinuous near it: Voliro eq 23; 2510.08119 intro and section 3 (Lipschitz constant of the orientation is bounded only if |F_j| stays above a minimum, their condition C2: |F_j + s_j(d)| >= epsilon_2, enforced by the nullspace smoothing term). +- 2510.08119 section 5 numerical examples (marine vessel with 3 azimuth thrusters; quadcopter) report orientation jumps when the pseudo-inverse solution crosses the singular point, and smoothing removing them. Their Figure 4 text: "the smoothing action appears when a discontinuity in azimuth orientation is close." +- The plain virtual-actuator formulation has the zero-thrust ambiguity because the unknown F carries the angle implicitly; the output angle command is a function of the solution. (Inferred from eqs 22-23 plus the above.) + +## 3. Question (c): games + +Retrieval for games was by web search of wiki and forum pages; no engine source code was read. + +- Kerbal Space Program: engines have a `ModuleGimbal` with configurable range (typically a few degrees; kerbal.wiki Part_modules example LV-909 shows about 4 degrees each way in pitch and yaw), per-axis response toggles (kOS docs: gimbal responds to pitch, yaw, roll controls separately; LIMIT suffix; LOCK snaps to zero), a response-speed field whose effect is unclear (a forum thread, topic 46553, reports no observable difference between speeds 1 and 100; a reply says the speed field is used only when `useGimbalResponseSpeed` is true). Roll participation of off-axis engines was added in 0.24 (forum summary). Gimbal acts only while the engine burns; RCS and reaction wheels cover the rest (forum posts). KerbCom Avionics mod added a solver balancing engine gimbals, reaction wheels and RCS (forum announcement; algorithm not retrieved). KSP's own internal allocation was not found in any retrieved source. +- Space Engineers: without mods, thrust is applied through the grid's centre of mass so offset thrusters make no torque (Keen support thread "Realistic Thrusters // Thruster Torque & Thrust Allocation", forum answers). Vectoring is done by mounting thrusters on rotors/sub-grids and scripting (Whip's, Vector Thrust OS named by players). The "Realistic Thrusters" mod/suggestion is the only torque-from-offset treatment found. +- Avorion: Directional Thruster block with nozzles on two opposing sides, aimed by rotating the block, affecting only that axis, 2.5x thrust along it (wiki; players report 3x); regular thrusters have nozzles on all six sides; thruster output scales against ship mass; turning authority comes from distance to centre of mass (wiki, developer post). No continuous gimbal found. +- Starsector: movement stats only (wiki: acceleration, turn acceleration, max turn rate as hull-mod multipliers); flux does not gate movement. No thrust allocation. +- Children of a Dead Earth: store/feature text says moment of inertia, delta-v and acceleration are computed; searches found no description of gimbals or RCS allocation. Nothing established. +- Orbiter (simulator, not shipped game with gimbals evidence): forum "Simplex-based thruster control" (thread 25232) describes an RCS class using the simplex method on arbitrary thruster layouts "no groups", simultaneous rotation and translation, commands scaled linearly when out of reach; 0-15 ms on 22 thrusters; occasional instability with unrealistically large torques; no mention of gimbals. ESA ATV uses simplex-based jet selection precomputed into a lookup table (search result [not retrieved]). +- No retrieved source describes a shipped game doing a physically allocated vectored thrust with an optimiser. (A negative result of limited searching, not proof of absence.) + +## 4. Question (d): shared-angle actuators (one gimbal per row) + +- No retrieved source formulates allocation for several thrusters sharing one tilt angle with individual magnitudes. This was searched directly (control allocation, shared tilt, common tilt, group of thrusters one gimbal) and returned nothing matching. +- Nearest found: TU Delft thesis snippet on a quadcopter with a single gimbal-style common tilt (Zheng et al.): two extra servos and spherical joints tilt the whole rotor set so the combined thrust vector is steerable and thrust is attitude-independent. The retrieved excerpt does not describe the allocation. [not retrieved in full] +- Structure of the problem as stated by the established formulations (inferred): with one common angle alpha per row, row member i produces force t_i (cos alpha, sin alpha). In virtual-actuator terms every member's component pair (F_v,i, F_l,i) must have the same ratio tan(alpha). The linear-component trick's advantage (angle free per actuator, constant matrix) is lost because the ratio equality across members is bilinear in (t_i, alpha); the formulation with per-thruster free components would overcount freedom. If the row's members have equal nominal directions, the row's total force and torque are (sum of t_i) and (sum of t_i * lever_i) (inferred), which makes the row's angle a single nonlinear variable shared by several linear throttles. +- Related structural facts in sources: 2507.18309 section 2 mixes fixed-direction thrusters (polar form, magnitude only) and azimuth thrusters (rectangular form, two components) in one allocation; this covers per-thruster angle plus fixed thrusters, not shared angle. +- Sequential treatment of coupled problems exists in the literature: the tilt-rotor papers' cascaded or iterated split (1.3). A row-shared angle solved as an outer scalar search with an inner bounded linear solve is not described in any retrieved source (inferred as one possible shape; no source). + +## 5. Converged / disagrees + +Converged (stated by two or more retrieved sources): + +1. Treating each tilting actuator as 2 or 3 linear force components with a constant allocation matrix, then recovering magnitude and angle by norm and atan2, is a standard approach: Voliro 1801.04581 (eq 16-23), Sørdalen extended thrust as cited in 2510.08119 and 2507.18309, Mukwege 2510.08119 (F in R^(3m)). +2. The recovered angle is discontinuous or undefined near zero component magnitude: Voliro (eq 23 and the 90 degree roll example), 2510.08119 (intro, section 3, section 5), 2507.18309 (azimuth penalty motivation). +3. Servo slowness is not represented by the linear trick alone: Voliro (explicit statement) and 2510.08119 (finite turning rate discussion) agree; solutions in the sources add a rate constraint (2510.08119 section 4), a rate-updated bound (2406.06130 section 4.1), a nullspace smoothing term (2510.08119), or a rate-limited reference filter (2507.18309). +4. Rate limits enter as per-sample bounds centred on the previous actuator state or previous solution: 2406.06130 (u_min/u_max updated from current state and rate), 2510.08119 (|F_i - F_i0| <= bound). +5. Tilt or pointing limits are cones in force space and convex; box approximations of the components are the cheap, conservative form: 2510.08119 (cone C_i, eqs 40-42). +6. Redundancy yields non-unique solutions chosen by a secondary objective (energy, nullspace, rest configuration): Voliro, 2510.08119, 2507.18309, 2406.06130. + +Disagrees / differs across sources: + +1. Linear vs nonlinear: Voliro rejects nonlinear least squares for compute cost; the marine SQP line (Johansen et al. 2004, MARIN) accepts a local QP per sample with a singularity penalty; 2510.08119 states the penalty fails when the thruster count is below the DOF count and around d = 0. +2. Where limits live: 2406.06130 puts the actuator box in the controller (NMPC) and uses a plain pseudo-inverse allocator; 2510.08119 puts constraints in the allocator QP/QCQP; 2507.18309 puts rate limitation in a downstream reference filter. +3. Singularity handling: penalty on det(J J^T) (Johansen 2004) vs nullspace rest configuration (2510.08119) vs differential allocation avoiding the algebraic map (Cuniato, snippet only) vs forbidden-zone removal (marine; Valčić says it causes non-convexity and large swings and proposes modelling interaction as losses instead). +4. Solvers: simplex/LP with scaling of out-of-reach commands (Orbiter thread) vs QP/active set (Härkegård, Bodson) vs SQP (Johansen, TU Delft) vs closed-form pseudo-inverse (Voliro, 2406.06130). +5. Games: Space Engineers applies thrust through the centre of mass by default (no torque from offset); Orbiter and the ATV approach allocate by simplex from true positions; KSP gimbals are small-range controls fed by control inputs per axis. They do not agree on whether positional torque is physically computed. + +## 6. Sources + +Read in full or in relevant part (text extracted): +- Kamel, Verling et al., "Voliro: An Omnidirectional Hexacopter With Tiltable Rotors", arXiv 1801.04581. https://arxiv.org/pdf/1801.04581 +- Mukwege, Nguyen, Garone, "General formulation of an analytic and Lipschitz continuous control allocation for thrust-vectored controlled rigid-bodies", arXiv 2510.08119v3 (Apr 2026). https://arxiv.org/pdf/2510.08119 +- Maneuvering-based Dynamic Thrust Allocation for Fully-Actuated Vessels, arXiv 2507.18309. https://arxiv.org/pdf/2507.18309 +- Nonlinear Model Predictive Control of Tiltrotor Quadrotors with Feasible Control Allocation, arXiv 2406.06130. https://arxiv.org/abs/2406.06130 +- Sorge, Ciresola, Michieletto, Cenedese, "Dynamic Control Allocation for Dual-Tilt UAV Platforms", arXiv 2604.05677 (text extracted, only keyword-checked; no claim above rests on it). https://arxiv.org/pdf/2604.05677 +- Constrained Dynamic Control Allocation in the Presence of Singularity and Infeasible Solutions, arXiv 1607.05209 (abstract only). https://arxiv.org/abs/1607.05209 +- Aetheria ThrustAllocator.cs, branch eureka/aetheria-release-thrust-allocator-core, commit 1fc98bdc. + +Cited, not retrieved (bibliographic or snippet only): +- Johansen & Fossen, "Control allocation - A survey", Automatica 49(5):1087-1103, 2013, DOI 10.1016/j.automatica.2013.01.035. Full text not found (NTNU URLs returned HTML stubs). +- Johansen, Fossen, Berge, IEEE TCST 12(1):211-216, 2004. +- Sørdalen, "Optimal thrust allocation for marine vessels", Control Engineering Practice 5(9):1223-1231, 1997. +- Härkegård & Glad, "Resolving actuator redundancy - optimal control vs. control allocation", Automatica 41(1):137-144, 2005; Härkegård active-set allocation as cited in Bodson & Frost (NASA NTRS 20100024149 appeared in search results). +- Cuniato et al., IEEE T-RO, differential allocation for tiltable-rotor platforms (search summary). +- Valčić & Prpić-Oršić, forbidden-zone handling in thrust allocation (CROSBI 838045, 899099). +- MARIN, "An advanced thrust allocation algorithm for DP applications" (search result). +- TU Delft thesis on dual-axis tilt-rotor quadplanes, https://repository.tudelft.nl/file/File_3e64f087-21b4-458c-8fc6-6ea6559a1bbe +- KSP: https://kerbal.wiki/index.php/Part_modules, https://forum.kerbalspaceprogram.com/topic/46553-engine-gimbal-reaction-speed, https://ksp-kos.readthedocs.io/en/latest/structures/vessels/gimbal.html (the wiki Gimbal page was blocked by bot protection). +- Space Engineers: https://support.keenswh.com/spaceengineers/pc/topic/45350-realistic-thrusters-thruster-torque-thrust-allocation +- Avorion: https://avorion.fandom.com/wiki/Directional_Thruster +- Starsector: https://starsector.wiki.gg/wiki/Auxiliary_Thrusters +- Orbiter: https://orbiter-forum.com/threads/simplex-based-thruster-control.25232 From fe9da2e0af3d447a4efa1e3d81d4b1e61ad56000 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 06:52:06 +0200 Subject: [PATCH 43/49] Test AStoreRefusesAMissingCatalog (red: AetheriaStores.Open does not yet refuse) Builds; the new test fails against the current Open. Co-Authored-By: Claude Opus 5.5 --- .../Aetheria.Shared.Tests/AetheriaStoresTests.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs b/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs index e0fec193d..5b9cdacf2 100644 --- a/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs +++ b/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs @@ -200,6 +200,22 @@ public sealed class AetheriaStoresTests : IDisposable Assert.Null(record); } + // A shipped player with no catalog must not boot into an empty game: a read-only open of a missing path throws + // naming the path, and does not create the file. A writable open still seeds an empty catalog. + [Fact] + public void AStoreRefusesAMissingCatalog() + { + var missing = Path.Combine(_root, "absent", "Aetheria.cc"); + + var error = Assert.Throws(() => AetheriaStores.Open(missing)); + Assert.Contains(missing, error.Message); + Assert.False(File.Exists(missing)); + Assert.False(Directory.Exists(Path.GetDirectoryName(missing))); + + using (var seeded = AetheriaStores.Open(missing, catalogWritable: true)) + Assert.Empty(seeded.GetAll()); + } + private static string Hash(string path) => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))); private static string[] SchemaNames(string path) From b997b80f81de7a4399d4b14a004ab37aac9b1db7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 06:55:32 +0200 Subject: [PATCH 44/49] AetheriaBuild.Build: one player entry point; AetheriaStores.Open refuses a missing catalog AetheriaBuild (Assets/Editor) sets the version, builds Addressables, builds the Win64 player, stages GameData through PlayerStaging, publishes AetherDb to ModTools and removes the Burst debug folder; any failure exits 1 naming the step. PlayerStaging is Unity-free and linked into Aetheria.Shared.Tests. A read-only AetheriaStores.Open of a nonexistent catalog throws. Builds on the headless side; the Unity .meta files and the batchmode run follow. Co-Authored-By: Claude Opus 5.5 --- Assets/Editor/AetheriaBuild.cs | 106 ++++++++++++++++++ Assets/Editor/PlayerStaging.cs | 42 +++++++ Assets/Scripts/ServerShared/AetheriaStores.cs | 4 + .../Aetheria.Shared.Tests.csproj | 2 + .../PlayerStagingTests.cs | 101 +++++++++++++++++ 5 files changed, 255 insertions(+) create mode 100644 Assets/Editor/AetheriaBuild.cs create mode 100644 Assets/Editor/PlayerStaging.cs create mode 100644 tests/Aetheria.Shared.Tests/PlayerStagingTests.cs diff --git a/Assets/Editor/AetheriaBuild.cs b/Assets/Editor/AetheriaBuild.cs new file mode 100644 index 000000000..26e56813e --- /dev/null +++ b/Assets/Editor/AetheriaBuild.cs @@ -0,0 +1,106 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using UnityEditor; +using UnityEditor.AddressableAssets.Settings; +using UnityEditor.Build.Reporting; +using Debug = UnityEngine.Debug; + +// The one way to make a Win64 player directory. Owns what the directory contains: Aetheria.exe and its data, +// GameData/Aetheria.cc, GameData/Narrative/**, GameData/Mods/** and ModTools/AetherDb. Manual builds, smokes and +// release cuts all call this and stage nothing themselves. +// +// Unity -batchmode -quit -projectPath -executeMethod AetheriaBuild.Build +// -aetheriaVersion -aetheriaOut -logFile +// +// AetherDb is published from tools/AetherDb, so the headless build's CULTLIB_ROOT / CULTMATH_ROOT (Directory.Build.props) +// must be set for dotnet when the defaults do not hold the pinned revisions. +// Any failure logs "AetheriaBuild FAILED at " and exits 1. +public static class AetheriaBuild +{ + private static readonly Regex SemVer = new Regex(@"^\d+\.\d+\.\d+([-+][0-9A-Za-z.-]+)?$"); + + public static void Build() + { + var step = "arguments"; + try + { + var version = Argument("-aetheriaVersion"); + if (!SemVer.IsMatch(version)) throw new InvalidOperationException("-aetheriaVersion is not a semver"); + var output = Path.GetFullPath(Argument("-aetheriaOut")); + var repoRoot = Directory.GetParent(UnityEngine.Application.dataPath).FullName; + + step = "version"; + PlayerSettings.bundleVersion = version; + + step = "addressables"; + AddressableAssetSettings.BuildPlayerContent(out var content); + if (!string.IsNullOrEmpty(content.Error)) throw new InvalidOperationException("Addressables build reported an error: " + content.Error); + + step = "player"; + Directory.CreateDirectory(output); + var scenes = EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(scene => scene.path).ToArray(); + if (scenes.Length == 0) throw new InvalidOperationException("EditorBuildSettings has no enabled scene"); + var report = BuildPipeline.BuildPlayer(new BuildPlayerOptions + { + scenes = scenes, + locationPathName = Path.Combine(output, "Aetheria.exe"), + target = BuildTarget.StandaloneWindows64, + options = BuildOptions.None, + }); + if (report.summary.result != BuildResult.Succeeded || report.summary.totalErrors != 0) + throw new InvalidOperationException($"BuildPlayer ended {report.summary.result} with {report.summary.totalErrors} errors"); + + step = "gamedata"; + PlayerStaging.Stage(repoRoot, output); + + step = "aetherdb"; + Publish(repoRoot, Path.Combine(output, "ModTools")); + + step = "burst"; + var burst = Path.Combine(output, "Aetheria_BurstDebugInformation_DoNotShip"); + if (Directory.Exists(burst)) Directory.Delete(burst, true); + + Debug.Log($"AetheriaBuild OK {version} {output}"); + } + catch (Exception error) + { + Debug.LogError($"AetheriaBuild FAILED at {step}: {error.GetType().Name}: {error.Message}"); + EditorApplication.Exit(1); + return; + } + EditorApplication.Exit(0); + } + + private static void Publish(string repoRoot, string modTools) + { + var info = new ProcessStartInfo("dotnet") + { + Arguments = $"publish tools/AetherDb -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o \"{modTools}\"", + WorkingDirectory = repoRoot, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + using (var process = Process.Start(info)) + { + var errors = process.StandardError.ReadToEndAsync(); + var text = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + if (process.ExitCode != 0) + throw new InvalidOperationException($"dotnet publish exited {process.ExitCode}:\n{text}\n{errors.Result}"); + } + if (!File.Exists(Path.Combine(modTools, "AetherDb.exe"))) + throw new InvalidOperationException("dotnet publish produced no ModTools/AetherDb.exe"); + } + + private static string Argument(string name) + { + var args = Environment.GetCommandLineArgs(); + var at = Array.IndexOf(args, name); + if (at < 0 || at + 1 >= args.Length) throw new InvalidOperationException(name + " is required"); + return args[at + 1]; + } +} diff --git a/Assets/Editor/PlayerStaging.cs b/Assets/Editor/PlayerStaging.cs new file mode 100644 index 000000000..4698130f6 --- /dev/null +++ b/Assets/Editor/PlayerStaging.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; + +// What a player directory holds beside the executable's data: GameData/Aetheria.cc, GameData/Narrative/** and, when +// the repo has one, GameData/Mods/**. Nothing else under GameData ships (run.cc and player.cc are the player's own; +// SoundbanksInfo.json is read by no code). Unity-free so the headless suite can prove the rules; AetheriaBuild is the +// only caller. +internal static class PlayerStaging +{ + public const string Catalog = "Aetheria.cc"; + public const string Narrative = "Narrative"; + public const string Mods = "Mods"; + + // Replaces /GameData with a copy of the three shipped entries of /GameData. + // Throws InvalidOperationException naming the entry when the catalog or Narrative is absent. + public static void Stage(string repoRoot, string playerDir) + { + var source = Path.Combine(repoRoot, "GameData"); + var destination = Path.Combine(playerDir, "GameData"); + var catalog = Path.Combine(source, Catalog); + var narrative = Path.Combine(source, Narrative); + var mods = Path.Combine(source, Mods); + + if (!File.Exists(catalog)) throw new InvalidOperationException($"GameData/{Catalog} is missing; a player never ships without its catalog."); + if (!Directory.Exists(narrative)) throw new InvalidOperationException($"GameData/{Narrative} is missing."); + + if (Directory.Exists(destination)) Directory.Delete(destination, true); + Directory.CreateDirectory(destination); + File.Copy(catalog, Path.Combine(destination, Catalog)); + CopyTree(narrative, Path.Combine(destination, Narrative)); + if (Directory.Exists(mods)) CopyTree(mods, Path.Combine(destination, Mods)); + } + + private static void CopyTree(string from, string to) + { + Directory.CreateDirectory(to); + foreach (var directory in Directory.GetDirectories(from, "*", SearchOption.AllDirectories)) + Directory.CreateDirectory(Path.Combine(to, directory.Substring(from.Length + 1))); + foreach (var file in Directory.GetFiles(from, "*", SearchOption.AllDirectories)) + File.Copy(file, Path.Combine(to, file.Substring(from.Length + 1))); + } +} diff --git a/Assets/Scripts/ServerShared/AetheriaStores.cs b/Assets/Scripts/ServerShared/AetheriaStores.cs index f265151c9..e2cdcd631 100644 --- a/Assets/Scripts/ServerShared/AetheriaStores.cs +++ b/Assets/Scripts/ServerShared/AetheriaStores.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using GameCult.Caching; using GameCult.Caching.MessagePack; @@ -15,6 +16,9 @@ public static class AetheriaStores // no records at all, a seed store being authored from nothing; a populated catalog is held to it however it opens. public static CultCache Open(string catalogPath, string runPath = null, string playerPath = null, bool catalogWritable = false) { + // A read-only open of an absent catalog would hydrate an empty game; a shipped player missing its catalog stops here. + if (!catalogWritable && !File.Exists(catalogPath)) + throw new InvalidOperationException($"Catalog {catalogPath} does not exist; a read-only open never invents one."); var cache = new CultCache(); try { diff --git a/tests/Aetheria.Shared.Tests/Aetheria.Shared.Tests.csproj b/tests/Aetheria.Shared.Tests/Aetheria.Shared.Tests.csproj index 8e36142f1..bad1c9b86 100644 --- a/tests/Aetheria.Shared.Tests/Aetheria.Shared.Tests.csproj +++ b/tests/Aetheria.Shared.Tests/Aetheria.Shared.Tests.csproj @@ -11,6 +11,8 @@ + + diff --git a/tests/Aetheria.Shared.Tests/PlayerStagingTests.cs b/tests/Aetheria.Shared.Tests/PlayerStagingTests.cs new file mode 100644 index 000000000..06af8406f --- /dev/null +++ b/tests/Aetheria.Shared.Tests/PlayerStagingTests.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; +using System.Linq; +using Xunit; + +// A player directory's GameData is exactly the catalog, Narrative/** and (when the repo has them) Mods/**. +public sealed class PlayerStagingTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-staging-" + Guid.NewGuid().ToString("N")); + private string Repo => Path.Combine(_root, "repo"); + private string Player => Path.Combine(_root, "player"); + private string Source(string relative) => Path.Combine(Repo, "GameData", relative); + private string Staged(string relative) => Path.Combine(Player, "GameData", relative); + + public PlayerStagingTests() + { + Write(Source("Aetheria.cc"), "catalog-bytes"); + Write(Source("Narrative/Main.ink"), "main"); + Write(Source("Narrative/Quests/Deep/First.ink"), "first"); + Write(Source("SoundbanksInfo.json"), "{}"); + Write(Source("run.cc"), "a developer's run"); + Write(Source("player.cc"), "a developer's settings"); + Directory.CreateDirectory(Player); + } + + public void Dispose() => Directory.Delete(_root, true); + + [Fact] + public void StagesTheCatalogAndNarrativeFileForFile() + { + PlayerStaging.Stage(Repo, Player); + + Assert.Equal("catalog-bytes", File.ReadAllText(Staged("Aetheria.cc"))); + Assert.Equal("main", File.ReadAllText(Staged("Narrative/Main.ink"))); + Assert.Equal("first", File.ReadAllText(Staged("Narrative/Quests/Deep/First.ink"))); + } + + [Fact] + public void StagesModsWhenTheRepoHasThemAndNoModsFolderWhenItDoesNot() + { + PlayerStaging.Stage(Repo, Player); + Assert.False(Directory.Exists(Staged("Mods"))); + + Write(Source("Mods/ship-a/ship.cc"), "ship"); + Write(Source("Mods/ship-a/mesh/hull.glb"), "hull"); + PlayerStaging.Stage(Repo, Player); + + Assert.Equal("ship", File.ReadAllText(Staged("Mods/ship-a/ship.cc"))); + Assert.Equal("hull", File.ReadAllText(Staged("Mods/ship-a/mesh/hull.glb"))); + } + + [Fact] + public void StagesNothingElseFromGameData() + { + PlayerStaging.Stage(Repo, Player); + + var staged = Directory.GetFileSystemEntries(Path.Combine(Player, "GameData")).Select(Path.GetFileName).OrderBy(name => name).ToArray(); + Assert.Equal(new[] { "Aetheria.cc", "Narrative" }, staged); + } + + [Fact] + public void ReplacesWhatAnEarlierBuildLeftInGameData() + { + Write(Staged("Aetheria.cc"), "stale"); + Write(Staged("run.cc"), "a played run"); + Write(Staged("Mods/gone/ship.cc"), "removed mod"); + + PlayerStaging.Stage(Repo, Player); + + Assert.Equal("catalog-bytes", File.ReadAllText(Staged("Aetheria.cc"))); + Assert.False(File.Exists(Staged("run.cc"))); + Assert.False(Directory.Exists(Staged("Mods"))); + } + + [Fact] + public void RefusesAMissingCatalogNamingItAndLeavesNoGameData() + { + File.Delete(Source("Aetheria.cc")); + + var error = Assert.Throws(() => PlayerStaging.Stage(Repo, Player)); + + Assert.Contains("GameData/Aetheria.cc", error.Message); + Assert.False(Directory.Exists(Path.Combine(Player, "GameData"))); + } + + [Fact] + public void RefusesMissingNarrative() + { + Directory.Delete(Source("Narrative"), true); + + var error = Assert.Throws(() => PlayerStaging.Stage(Repo, Player)); + + Assert.Contains("GameData/Narrative", error.Message); + } + + private static void Write(string path, string text) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllText(path, text); + } +} From e5a57dab9fe5aa65253ae08cdbf68d12893e5bed Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 07:17:13 +0200 Subject: [PATCH 45/49] AetheriaBuild: qualify UnityEditor.PlayerSettings (the game has a global PlayerSettings); Unity .meta files The previous commit did not compile in Unity (CS0117 on PlayerSettings.bundleVersion); this one does, and a batchmode run of AetheriaBuild.Build succeeded. Co-Authored-By: Claude Opus 5.5 --- Assets/Editor/AetheriaBuild.cs | 2 +- Assets/Editor/AetheriaBuild.cs.meta | 2 ++ Assets/Editor/PlayerStaging.cs.meta | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 Assets/Editor/AetheriaBuild.cs.meta create mode 100644 Assets/Editor/PlayerStaging.cs.meta diff --git a/Assets/Editor/AetheriaBuild.cs b/Assets/Editor/AetheriaBuild.cs index 26e56813e..7d3c891d2 100644 --- a/Assets/Editor/AetheriaBuild.cs +++ b/Assets/Editor/AetheriaBuild.cs @@ -33,7 +33,7 @@ public static class AetheriaBuild var repoRoot = Directory.GetParent(UnityEngine.Application.dataPath).FullName; step = "version"; - PlayerSettings.bundleVersion = version; + UnityEditor.PlayerSettings.bundleVersion = version; // the game has its own global PlayerSettings step = "addressables"; AddressableAssetSettings.BuildPlayerContent(out var content); diff --git a/Assets/Editor/AetheriaBuild.cs.meta b/Assets/Editor/AetheriaBuild.cs.meta new file mode 100644 index 000000000..833ad46ac --- /dev/null +++ b/Assets/Editor/AetheriaBuild.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d38caad223051494ba36c734ce79950d \ No newline at end of file diff --git a/Assets/Editor/PlayerStaging.cs.meta b/Assets/Editor/PlayerStaging.cs.meta new file mode 100644 index 000000000..71743d471 --- /dev/null +++ b/Assets/Editor/PlayerStaging.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 16dce8087f9149b44b6e294cb0851dd8 \ No newline at end of file From 387404c6eac996d612647f4d4ddf959088e1defc Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 07:27:31 +0200 Subject: [PATCH 46/49] AetheriaStoresTests: MissingCatalogGlobalIsLoud opens an existing empty catalog The test relied on a read-only open of a nonexistent file yielding an empty catalog, which AetheriaStores.Open now refuses; the fixture writes the empty catalog first. Builds; the suite is green with it. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs b/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs index 5b9cdacf2..099c3a259 100644 --- a/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs +++ b/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs @@ -116,7 +116,9 @@ public sealed class AetheriaStoresTests : IDisposable public void MissingCatalogGlobalIsLoud() { var empty = Path.Combine(_root, "empty.cc"); - using (AetheriaStores.Open(empty, catalogWritable: true)) { } + // An existing catalog with no records: a read-only open refuses a missing file before it checks globals. + using (var seed = AetheriaStores.Open(empty, catalogWritable: true)) seed.FlushAsync().Wait(); + Assert.True(File.Exists(empty)); var error = Assert.Throws(() => AetheriaStores.Open(empty)); Assert.Contains("aetheria.tests.catalogglobal", error.Message); using (AetheriaStores.Open(Catalog)) { } From 1521c68a7922dfbc7def251a741c51edb6dbdd67 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 07:28:04 +0200 Subject: [PATCH 47/49] AetheriaStores.Open: a missing catalog throws FileNotFoundException The test assembly registers a catalog [CultGlobal], so the global check already threw on a missing file there and AStoreRefusesAMissingCatalog passed against the old Open (run at fe9da2e: 12 passed). The refusal is now a FileNotFoundException naming the path, and the test pins that type, which the global check never throws. Builds. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/AetheriaStores.cs | 2 +- tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Assets/Scripts/ServerShared/AetheriaStores.cs b/Assets/Scripts/ServerShared/AetheriaStores.cs index e2cdcd631..92f8e3f90 100644 --- a/Assets/Scripts/ServerShared/AetheriaStores.cs +++ b/Assets/Scripts/ServerShared/AetheriaStores.cs @@ -18,7 +18,7 @@ public static class AetheriaStores { // A read-only open of an absent catalog would hydrate an empty game; a shipped player missing its catalog stops here. if (!catalogWritable && !File.Exists(catalogPath)) - throw new InvalidOperationException($"Catalog {catalogPath} does not exist; a read-only open never invents one."); + throw new FileNotFoundException($"Catalog {catalogPath} does not exist; a read-only open never invents one.", catalogPath); var cache = new CultCache(); try { diff --git a/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs b/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs index 099c3a259..4d7611b9a 100644 --- a/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs +++ b/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs @@ -209,7 +209,8 @@ public sealed class AetheriaStoresTests : IDisposable { var missing = Path.Combine(_root, "absent", "Aetheria.cc"); - var error = Assert.Throws(() => AetheriaStores.Open(missing)); + // FileNotFoundException, not the catalog-global check: that one also throws on a missing file whenever a global is registered. + var error = Assert.Throws(() => AetheriaStores.Open(missing)); Assert.Contains(missing, error.Message); Assert.False(File.Exists(missing)); Assert.False(Directory.Exists(Path.GetDirectoryName(missing))); From 86fb594031a6e9168367ae3e585994e148d309ec Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 07:37:49 +0200 Subject: [PATCH 48/49] Release map: vectored thruster rows (probes V1-V8, model rows, cut order) Imagination pass (session self-2026-10-10-ag): the row gimbal is sim data, one shared-throttle actuator per row in the allocator's single solve; specs vector-rows-record r2 and vector-rows-allocator r1. Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 146 +++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index 08528d188..80ec3dd98 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -7623,3 +7623,149 @@ retire-firing-arc r1, ships-addon-link-rows r1; link-rig-record r3; ships-addon- `mind-commit-0c23fa01...` (ships-addon-link-package r2, link-rig-presenter r2, link-calibre-judge r2, question `hardpoint-arc-caps`, withdrew question `calibre-arc-caps`) and `mind-commit-40788d58...` (ships-addon-link-package r3, r2's arm smoke pins fixed). context-pack read every anchor SAME at `acea1aa7`. + +## Vectored thruster rows + +Pass: Imagination (`imagination-aeth-vectoring`), session `self-2026-10-10-ag`. Body: `GameCult/Aetheria` origin/master +`30d02719`; the allocator at `1fc98bdc` (branch `eureka/aetheria-release-thrust-allocator-core`, read-only, a fix +batch in flight); the rig record at `562de34c` (branch `eureka/aetheria-release-link-rig-record`, Soul pass running); +CultMath `BoundedLeastSquares` at CultLib origin/main `50512e6e`. One design owns follow-ups `vectored-thruster-rows` +(operator: "if that dine and dash could tilt that vector for each row of thrusters it would be even more hyper +maneuverable"; a gimbal per row, not per thruster) and `gimballed-thrusters`. Prior art: +`docs/research/thrust-vectoring-prior-art.md` at `30d02719` (cited as VP, by section). + +### Body facts + +- **V1. A column is linear in the push direction.** The allocator's column is xy = push * thrust / mass and + z = `Torque` * thrust * `TorqueMultiplier` / mass (spec `cut-thrust-allocator.r1`, adds; `ThrustAllocator.cs:8-18` + at `1fc98bdc`). `Torque` is `-dot(normalize(toCenter), float2(1,0).Rotate(rotation))` (`Thruster.cs:62-71`), linear + in the direction vector. So a thruster tilted by angle a has column c(a) = cos a * c(0) + sin a * c(pi/2): two fixed + columns per thruster carry every tilt. This is VP 1.1's virtual-actuator trick, exact for Aetheria's planar plant. +- **V2. BoundedLeastSquares has box bounds only** (`BoundedLeastSquares.cs`, CultLib `50512e6e`: `lo <= x <= hi`, + dense primal active set, iteration cap 100, warm start in `x`). A gimbal wedge |v| <= tan(theta) u is not a box + (VP 2). The design below never needs the wedge: the per-tick slew window in the row's own frame is narrow (3 deg at + 90 deg/s and 30 Hz, 12 deg at 360 deg/s), so v is boxed by the sine of the window and the recovered angle is + clamped to the window and the gimbal. No CultMath capability is missing, so no cultlib-gaps follow-up. +- **V3. The sim already has the data seam.** `HardpointData.FiringArc` (key 6) is the hardpoint's arc + (`ItemData.cs:567-569`); `EquippedItem.Hardpoint` gives a behaviour its hardpoint (`Entity.cs:1688`, read by + `FireControl.ArcFor`, `FireControl.cs:36-41`). `HardpointData` uses keys 0-6; `ThrusterData` keys 1-5 + (`Thruster.cs:12-27`; `BehaviorData` owns key 0). At `562de34c` the judge refuses a `thruster-emitter` anchor on a + joint, naming follow-up `gimballed-thrusters` (`ShipAuthoring.cs:360-363`), and checks that weapon mounts on one + joint share `Rotation` and `FiringArc` and that the chain's yaw reach covers the arc (`:370-385`). +- **V4. The gimbal is a sim fact.** By V1 the angle changes push and turn, which the allocator and the evasion + envelope read. In V6, fixed rows leave forward and reverse at E 0.361 on the small hull; tilting rows take both to + 0.000. So the row angle, its limit and its slew are simulation state and stats (target invariant `sim-owns-facts`); + the joint only draws `Thruster.Angle` (rulings `rig-is-presentation`, `mount-arc-gun-traverse`). +- **V5. Probe harness.** Scratch C# console at `scratchpad/imagination-aeth-vectoring/harness` (local commit + `b5162ab`, then a v2 that fixed M4's scoring and added M3c and M5), `Program.cs` plus a verbatim copy of + `BoundedLeastSquares.cs` from `50512e6e`, run on Yggdrasil through `ygg-verify.sh` (`dotnet run -c Release`, image + `mcr.microsoft.com/dotnet/sdk:10.0`), 2026-10-10, exit 0 both runs (`run1.log`, `run2.log` beside it). The core + solve copies `ThrustAllocator` at `1fc98bdc` exactly (weights 100 and 1000, L1 split error variables, ridge 0.1, + KKT 1e-12, cap 100); every method shares it and shares one demand scale (V7). Hulls: *small*, two mains (accel 10) + and two side rows of four (accel 2, pushing across the hull, gimbal +-45 deg); *large*, four mains and four rows of + eight (36 thrusters). 30 Hz, 90 ticks per intent from rows at 0, 12 intents (full strafes, forward, reverse, two + diagonals, full turns, forward plus turn, strafe plus half turn, two partial). E is the realized wrench's error + against the demand, normalised per axis by the box extreme. REF is a brute-force grid over both row angles + (37 x 37, no slew limit) with the fixed-column solve inside, minimum weighted error. +- **V6. Results** (mean E over the 12 intents after 90 ticks; worst single solve's iterations; us per tick on + Yggdrasil): + + | Method | small 90 deg/s | small 360 deg/s | large 90 deg/s | solves/tick | worst iters (large) | us/tick (large) | + |---|---|---|---|---|---|---| + | REF grid (static optimum, shared row angle) | 0.134 | 0.134 | n/a | 1369 | n/a | n/a | + | M0 fixed rows (today) | 0.298 | 0.298 | 0.310 | 1 | 95 | 36 | + | M1 row as one actuator: shared throttle, (u, v) in the row frame, slew-boxed | 0.142 | 0.138 | 0.133 | 1 | 20 | 3 | + | M2 cascade: per-thruster (u, v), one angle per row, re-solve | 0.137 | 0.131 | 0.131 | 2 | 95 | 267 | + | M3 angle step linearised at last tick's throttles, one solve | 0.201 | 0.196 | 0.215 | 1 | 95 | 74 | + | M3c as M3, idle rows linearised at throttle .25 | 0.138 | 0.136 | 0.132 | 1 | 100 (2 IterationLimit) | 79 | + | M5 M1's angle, then per-thruster re-solve | 0.142 | 0.138 | 0.131 | 2 | 95 | 44 | + | M4 per-thruster gimbals (no row constraint; upper bound) | 0.138 | 0.135 | 0.129 | 1 | 89 | 328 | + + No method produced NaN. M1, M2, M3c, M4 and M5 reach E 0.000 on forward and reverse (M0 and M3: 0.361). The large + hull's 95-iteration solves are the fixed per-thruster solve itself (M0 is also 95): finding + `cut-thrust-allocator-core.s1.large-hull-degradation`, follow-up `allocator-large-hull`. M1 collapses each row to + two variables (12 on the large hull instead of 36) and stays at 20 iterations, which is the "group identical + columns" remedy that follow-up names, arrived at for free. +- **V7. The demand scale must be the gimbal reach.** The allocator scales intent by box extremes it sums from the + columns it is given (`ThrustAllocator.cs:44-61`). With rows tilting, current-column extremes move every tick, so + "full forward" would change meaning as the rows swing. The probe scales by the reach: per thruster and half-axis, + the maximum of A cos a + B sin a over the gimbal (closed form: sqrt(A^2 + B^2) when atan2(B, A) lies inside the arc, + else the better end). Small hull: +y 31.3 (mains 20, eight row thrusters 2 sin 45 deg each), -y 11.3, +-x 8.0, + +-z 12.53. The same reach is the envelope's box (cut `envelope-from-columns`), so intent and evasion read one + geometry. +- **V8. Zero thrust: hold the angle, but never linearise through the throttle.** Schedule strafe right, idle, strafe + left, forward, idle, reverse, 30 ticks each, both hulls, 90 deg/s. M1 and M5: every step within the window (3.00 + deg max), idle drift 0.000 deg, idle throttle 0, no sign flips in the late half of any phase, and reverse ends at + -45/45 (correct). M2: one or two late sign flips, and 3 deg of idle drift on the large hull (its stage one aims + idle rows with v alone). M3 is trapped: a row at zero throttle has a zero angle column, so it never tilts, forward + stays at E 0.361 and reverse ends at 45/-45 (the wrong way). M3c unsticks it but chatters (15 late flips), fires + at full throttle while idle on the large hull and hits the cap. Holding the previous angle when the row's (u, v) + is near zero (|(u, v)| < 1e-6) is enough once (u, v) are free variables: v can aim an idle row without thrust, and + the box's overcount is useful there. + +### Model page rows + +| Kind | Identity | Lifecycle | Authority | +|---|---|---|---| +| Gimbal limit (`HardpointData.FiringArc`, key 6, on a `Thruster` hardpoint; question `vector-row-data-home`) | The hardpoint's mount id (`Transform`) on its hull. Full width in degrees, as for a gun; the row swings +-FiringArc/2 about `Rotation`. 0 is a fixed thruster; thrusters get no `GameplaySettings.FiringArc` fallback. | Authored per hull with the other hardpoint fields; Save or Package writes it; a package edit changes it on Continue (moves no cell). | The ship author. The judge requires every member of a row to share it and the row's joint chain to reach it. The thruster reads it at activation. | +| Row (`HardpointData.Row`, new key 7, string) | Unique within its hull; empty means not in a row. A `Thruster` hardpoint with `FiringArc` > 0 and no `Row` is a row of one. | Authored per hull; renaming a row changes no save (no save names a row; ruling `no-save-compatibility-before-players`). | The ship author. Judge: only on `Thruster` hardpoints; members share `Rotation` and a `FiringArc` > 0. Never derived from a joint (ruling `rig-is-presentation`). | +| Slew rate (`ThrusterData.Slew`, new key 6, `PerformanceStat`, deg/s) | A stat of the thruster design. | Authored per design; heat, durability and quality move it live like `Thrust`. A row slews at its slowest live member. | The gear designer, as a weapon's traverse is (ruling `mount-arc-gun-traverse`). | +| Live row angle (`Thruster.Angle`, runtime) | The thruster behaviour; every member of a row holds the same value. Radians from the hardpoint's `Rotation`. | 0 at `Activate`; moves at most slew x dt per tick; held while the row is idle; never saved (a load restarts at 0). | `ThrustAllocator` decides it; `Ship.Update` is its one writer. `Execute`, the envelope and presentation read it. The rig draws it and never writes it. | +| Row actuator (runtime) | The row, built at `Activate` from its members' hardpoints. | Rebuilt when the thruster set changes (the allocator's resize rule). A dead member's columns are zero, so the row keeps steering with the rest. | `ThrustAllocator` reads the summed along and across columns and the slew window, and decides one throttle and one angle step per row. | + +### Authority map + +- **Owner.** `ThrustAllocator` decides every fixed thruster's throttle and every row's throttle and angle step, in one + `BoundedLeastSquares` solve (question `vector-row-formulation`, recommended `shared-throttle-row`). +- **Inputs.** Intent (unchanged); each fixed thruster's column; per row, the summed column along and across its live + angle and its slew window [max(-arc/2 - angle, -slew dt), min(arc/2 - angle, slew dt)]; the reach (V7). +- **Outputs.** `Thruster.Axis` for every thruster (a row's members share the row's) and `Thruster.Angle` for every + row member, read by `Execute`, `PowerRequest`, the envelope and presentation. +- **Derived.** The reach and the envelope (one helper); a row's summed columns; the slew window. +- **No longer owners.** `Allocate` no longer sums its box extremes from the current columns; they come from the reach + helper. A joint, its limits and the IK pose decide nothing in the sim. +- **Forbidden writers.** Any `Angle` write outside `Ship.Update`; any read of `ShipJoint`, `ShipAnchor.Joint` or a + Unity transform in `Thruster`, `Ship` or `ThrustAllocator`; a second throttle solve or fallback mixer; an angle step + linearised through last tick's throttles (M3, V8). +- **Shared paths.** Player helm, agents, the throttle lock and tests reach rows only through intent. + `Thruster.Column(angle)` is the one geometry function `Execute`, the allocator and the envelope call. + +### Rationale + +- **Why one actuator per row.** M1 matches M5 (per-member throttles after the same angle) within 0.002 mean E on all + three sweeps, is within 0.008 of the static optimum, keeps the allocator's one solve (ruling + `one-weighted-solve-no-rotation-demand`; the thrust-allocator authority map forbids a second solve) and is the + cheapest in iterations and time. M2 is the most accurate (0.131) but doubles the variables, reaches 95 of 100 + iterations on 36 thrusters and drifts idle rows. What M1 gives up is differential throttle inside a row: on these + hulls it measured nothing, and a row that needs it can be authored as two rows. The operator decides: question + `vector-row-formulation`. +- **Why the limit is the hardpoint's arc and the slew the gear's.** It is ruling `mount-arc-gun-traverse` applied to + thrusters: how far a mount swings is ship authoring; how fast is the part's own stat. Reusing `FiringArc` adds no + field that the add-on, Studio and C4 must learn; its meaning (how far the mounted item may point from `Rotation`) + already fits. Question `vector-row-data-home` holds the alternatives. +- **Why the envelope counts the whole gimbal, not the slew.** The envelope already ignores the time to swing the + hull (cut `envelope-from-columns` sums box extremes); counting slew time only for rows would be a second, + inconsistent time model. If the pace pass finds slow gimbals over-credited, slew time enters the envelope for hull + turn and rows together. +- **Sequence.** Both cuts land after `thrust-allocator` and `envelope-from-columns`, whose `Ship.Update` wiring and + `RecalculateEnvelope` they change, and after `link-rig-record`, whose judge they extend. The rig and the add-on are + follow-up `vector-rows-presentation`: nothing authors a `Row` until the add-on does, so the record cut is inert + data until then. + +### Cut order + +1. `vector-rows-record` after `link-rig-record`, `thrust-allocator` and `envelope-from-columns`: + `HardpointData.Row`, `ThrusterData.Slew`, the judge's row rules, thruster anchors allowed on joints with the + reach check. +2. `vector-rows-allocator` after `vector-rows-record`, `thrust-allocator-core`, `thrust-allocator` and + `envelope-from-columns`: rows as one actuator each in the solve, `Thruster.Angle`, `Column(angle)`, the reach + helper for demand and envelope. Its `Ship.cs` anchors are written against the merged wiring at Hands time + (the spec says where). + +### Admitted + +2026-10-10 (`imagination-aeth-vectoring`): receipt `mind-commit-2829a7e1...` (cut_specs `cut-vector-rows-record.r1`, +`cut-vector-rows-allocator.r1`, questions `vector-row-formulation` and `vector-row-data-home`, follow-up +`vector-rows-presentation`) and `mind-commit-e1337a9c...` (`cut-vector-rows-record.r2` superseding r1: its +ShipValidationTests anchor named the wrong class). context-pack read every anchor SAME: record r2 at `562de34c`, +allocator r1 at `1fc98bdc`. From 9ae5fbb350375d1aa93bb55b24dbc028c01ab828 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 08:10:12 +0200 Subject: [PATCH 49/49] AetheriaStoresTests: MissingCatalogGlobalIsLoud copies the fixture catalog and removes its global Flushing an empty cache writes no file, so the previous fixture still hit the new missing-file refusal. Builds. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs b/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs index 4d7611b9a..bff3a1a6d 100644 --- a/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs +++ b/tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs @@ -116,9 +116,10 @@ public sealed class AetheriaStoresTests : IDisposable public void MissingCatalogGlobalIsLoud() { var empty = Path.Combine(_root, "empty.cc"); - // An existing catalog with no records: a read-only open refuses a missing file before it checks globals. - using (var seed = AetheriaStores.Open(empty, catalogWritable: true)) seed.FlushAsync().Wait(); - Assert.True(File.Exists(empty)); + // A catalog file that exists but lacks the global: the read-only open reaches the global check. + File.Copy(Catalog, empty); + using (var seed = AetheriaStores.Open(empty, catalogWritable: true)) + Assert.True(seed.Commit(batch => batch.Remove(seed.RefOf(seed.GetGlobal()).Key))); var error = Assert.Throws(() => AetheriaStores.Open(empty)); Assert.Contains("aetheria.tests.catalogglobal", error.Message); using (AetheriaStores.Open(Catalog)) { }