update
This commit is contained in:
52
Components/Crankshaft.cs
Normal file
52
Components/Crankshaft.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
// Components/Crankshaft.cs
|
||||
using System;
|
||||
|
||||
namespace FluidSim.Components
|
||||
{
|
||||
public class Crankshaft
|
||||
{
|
||||
public double AngularVelocity { get; set; } // rad/s
|
||||
public double CrankAngle { get; set; } // rad, 0 … 4π (four‑stroke cycle)
|
||||
public double PreviousAngle { get; private set; } // for TDC detection
|
||||
|
||||
public double Inertia { get; set; } = 0.2;
|
||||
public double FrictionConstant { get; set; } = 2.0; // N·m
|
||||
public double FrictionViscous { get; set; } = 0.005; // N·m per rad/s
|
||||
|
||||
private double externalTorque;
|
||||
|
||||
/// <param name="initialRPM">Idle speed before any combustion torque is applied.</param>
|
||||
public Crankshaft(double initialRPM = 400.0)
|
||||
{
|
||||
AngularVelocity = initialRPM * 2.0 * Math.PI / 60.0;
|
||||
CrankAngle = 0.0;
|
||||
PreviousAngle = 0.0;
|
||||
}
|
||||
|
||||
public void AddTorque(double torque) => externalTorque += torque;
|
||||
|
||||
public void Step(double dt)
|
||||
{
|
||||
// Save previous angle
|
||||
PreviousAngle = CrankAngle;
|
||||
|
||||
// Friction
|
||||
double friction = FrictionConstant * Math.Sign(AngularVelocity) + FrictionViscous * AngularVelocity;
|
||||
double netTorque = externalTorque - friction;
|
||||
double alpha = netTorque / Inertia;
|
||||
AngularVelocity += alpha * dt;
|
||||
|
||||
if (AngularVelocity < 0) AngularVelocity = 0; // stall
|
||||
|
||||
CrankAngle += AngularVelocity * dt;
|
||||
|
||||
// Wrap to [0, 4π)
|
||||
if (CrankAngle >= 4.0 * Math.PI)
|
||||
CrankAngle -= 4.0 * Math.PI;
|
||||
else if (CrankAngle < 0)
|
||||
CrankAngle += 4.0 * Math.PI;
|
||||
|
||||
externalTorque = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
215
Components/EngineCylinder.cs
Normal file
215
Components/EngineCylinder.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
// EngineCylinder.cs (in Core namespace)
|
||||
using System;
|
||||
using FluidSim.Components;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
public class EngineCylinder
|
||||
{
|
||||
public Volume0D Cylinder { get; private set; }
|
||||
private Crankshaft crankshaft;
|
||||
|
||||
private double bore, stroke, conRodLength, compressionRatio;
|
||||
private double pistonArea;
|
||||
private double V_disp, V_clear;
|
||||
private double maxOrificeArea;
|
||||
|
||||
private double valveOpenStart = 120.0 * Math.PI / 180.0;
|
||||
private double valveOpenEnd = 480.0 * Math.PI / 180.0;
|
||||
private double valveRampWidth = 30.0 * Math.PI / 180.0;
|
||||
public double OrificeArea => ValveLift() * maxOrificeArea;
|
||||
|
||||
public double TargetPeakPressure { get; set; } = 50.0 * 101325.0;
|
||||
private const double PeakTemperature = 2500.0;
|
||||
private bool burnInProgress = false;
|
||||
private double burnStartAngle; // full cycle angle when ignition began
|
||||
private double burnDuration = 40.0 * Math.PI / 180.0;
|
||||
private double targetBurnEnergy;
|
||||
private double totalBurnMass;
|
||||
private double preIgnitionMass, preIgnitionInternalEnergy;
|
||||
|
||||
private Random rand = new Random();
|
||||
public double MisfireProbability { get; set; } = 0.02;
|
||||
private bool misfireCurrent = false;
|
||||
|
||||
public int CombustionCount { get; private set; }
|
||||
public int MisfireCount { get; private set; }
|
||||
|
||||
public EngineCylinder(Crankshaft crankshaft,
|
||||
double bore, double stroke, double compressionRatio,
|
||||
double pipeArea, int sampleRate)
|
||||
{
|
||||
this.crankshaft = crankshaft;
|
||||
this.bore = bore;
|
||||
this.stroke = stroke;
|
||||
conRodLength = 2.0 * stroke;
|
||||
this.compressionRatio = compressionRatio;
|
||||
maxOrificeArea = pipeArea;
|
||||
pistonArea = Math.PI / 4.0 * bore * bore;
|
||||
|
||||
V_disp = pistonArea * stroke;
|
||||
V_clear = V_disp / (compressionRatio - 1.0);
|
||||
|
||||
// Initial compressed charge at TDC (no burn)
|
||||
double T_bdc = 300.0;
|
||||
double p_bdc = 101325.0;
|
||||
double V_bdc = V_clear + V_disp;
|
||||
double freshMass = p_bdc * V_bdc / (287.0 * T_bdc);
|
||||
double freshInternalEnergy = p_bdc * V_bdc / (1.4 - 1.0);
|
||||
double p_tdc = p_bdc * Math.Pow(V_bdc / V_clear, 1.4);
|
||||
|
||||
Cylinder = new Volume0D(V_clear, p_tdc, T_bdc * Math.Pow(V_bdc / V_clear, 1.4 - 1.0), sampleRate)
|
||||
{
|
||||
Gamma = 1.4,
|
||||
GasConstant = 287.0
|
||||
};
|
||||
Cylinder.Volume = V_clear;
|
||||
Cylinder.Mass = freshMass;
|
||||
Cylinder.InternalEnergy = p_tdc * V_clear / (1.4 - 1.0);
|
||||
|
||||
preIgnitionMass = Cylinder.Mass;
|
||||
preIgnitionInternalEnergy = Cylinder.InternalEnergy;
|
||||
}
|
||||
|
||||
// ---- Piston kinematics (uses full cycle angle for position) ----
|
||||
private (double volume, double dvdt) PistonKinematics(double cycleAngle)
|
||||
{
|
||||
// Slider-crank uses 0–2π, but we want the same motion for 0–2π (power/exhaust) and 2π–4π (intake/compression)
|
||||
double theta = cycleAngle % (2.0 * Math.PI);
|
||||
double R = stroke / 2.0;
|
||||
double cosT = Math.Cos(theta);
|
||||
double sinT = Math.Sin(theta);
|
||||
double L = conRodLength;
|
||||
|
||||
double s = R * (1 - cosT) + L - Math.Sqrt(L * L - R * R * sinT * sinT);
|
||||
double V = V_clear + pistonArea * s;
|
||||
|
||||
double sqrtTerm = Math.Sqrt(L * L - R * R * sinT * sinT);
|
||||
double dVdθ = pistonArea * (R * sinT + (R * R * sinT * cosT) / sqrtTerm);
|
||||
double dvdt = dVdθ * crankshaft.AngularVelocity;
|
||||
return (V, dvdt);
|
||||
}
|
||||
|
||||
// ---- Valve lift ----
|
||||
private double ValveLift()
|
||||
{
|
||||
double cycleRad = crankshaft.CrankAngle;
|
||||
if (cycleRad < valveOpenStart || cycleRad > valveOpenEnd)
|
||||
return 0.0;
|
||||
|
||||
double duration = valveOpenEnd - valveOpenStart;
|
||||
double ramp = valveRampWidth;
|
||||
double t = (cycleRad - valveOpenStart) / duration;
|
||||
double rampFrac = ramp / duration;
|
||||
|
||||
if (t < rampFrac)
|
||||
return t / rampFrac;
|
||||
else if (t > 1.0 - rampFrac)
|
||||
return (1.0 - t) / rampFrac;
|
||||
else
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
private double WiebeFraction(double angleFromIgnition)
|
||||
{
|
||||
if (angleFromIgnition >= burnDuration) return 1.0;
|
||||
double a = 5.0, m = 2.0;
|
||||
double x = angleFromIgnition / burnDuration;
|
||||
return 1.0 - Math.Exp(-a * Math.Pow(x, m + 1));
|
||||
}
|
||||
|
||||
// ---- Torque from pressure ----
|
||||
private double ComputeTorque()
|
||||
{
|
||||
double p = Cylinder.Pressure;
|
||||
double ambient = 101325.0;
|
||||
double force = (p - ambient) * pistonArea;
|
||||
if (force <= 0) return 0.0;
|
||||
|
||||
double theta = crankshaft.CrankAngle % (2.0 * Math.PI);
|
||||
double R = stroke / 2.0;
|
||||
double L = conRodLength;
|
||||
double sinT = Math.Sin(theta);
|
||||
double cosT = Math.Cos(theta);
|
||||
|
||||
double sqrtTerm = Math.Sqrt(L * L - R * R * sinT * sinT);
|
||||
double lever = R * sinT * (1.0 + (R * cosT) / sqrtTerm);
|
||||
return force * lever;
|
||||
}
|
||||
|
||||
public void Step(double dt)
|
||||
{
|
||||
double cycleAngle = crankshaft.CrankAngle;
|
||||
double prevAngle = crankshaft.PreviousAngle;
|
||||
|
||||
// ----- TDC crossing detection (power stroke) -----
|
||||
// Power stroke TDC occurs at angle 0 (mod 4π). We detect when PreviousAngle was near 4π and CrankAngle wraps to near 0.
|
||||
bool crossingTDC = (prevAngle > 3.8 * Math.PI && cycleAngle < 0.2 * Math.PI) // normal forward
|
||||
|| (prevAngle < 0.2 * Math.PI && cycleAngle > 3.8 * Math.PI); // (rare backward, ignore)
|
||||
|
||||
if (crossingTDC)
|
||||
{
|
||||
misfireCurrent = rand.NextDouble() < MisfireProbability;
|
||||
|
||||
// Fresh charge: trapped at BDC, compressed isentropically to V_clear
|
||||
double T_bdc = 300.0;
|
||||
double p_bdc = 101325.0;
|
||||
double V_bdc = V_clear + V_disp;
|
||||
double freshMass = p_bdc * V_bdc / (287.0 * T_bdc);
|
||||
double freshInternalEnergy = p_bdc * V_bdc / (1.4 - 1.0);
|
||||
double gamma = 1.4;
|
||||
double p_tdc = p_bdc * Math.Pow(V_bdc / V_clear, gamma);
|
||||
|
||||
Cylinder.Volume = V_clear;
|
||||
Cylinder.Mass = freshMass;
|
||||
Cylinder.InternalEnergy = p_tdc * V_clear / (gamma - 1.0);
|
||||
|
||||
preIgnitionMass = Cylinder.Mass;
|
||||
preIgnitionInternalEnergy = Cylinder.InternalEnergy;
|
||||
|
||||
if (misfireCurrent)
|
||||
{
|
||||
MisfireCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
double V = V_clear;
|
||||
targetBurnEnergy = TargetPeakPressure * V / (gamma - 1.0);
|
||||
totalBurnMass = TargetPeakPressure * V / (287.0 * PeakTemperature);
|
||||
burnInProgress = true;
|
||||
burnStartAngle = cycleAngle;
|
||||
CombustionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Burn progress -----
|
||||
if (burnInProgress)
|
||||
{
|
||||
double angleFromIgnition = cycleAngle - burnStartAngle;
|
||||
if (angleFromIgnition < 0) angleFromIgnition += 4.0 * Math.PI; // wrap if needed
|
||||
|
||||
if (angleFromIgnition >= burnDuration)
|
||||
{
|
||||
Cylinder.Mass = totalBurnMass;
|
||||
Cylinder.InternalEnergy = targetBurnEnergy;
|
||||
burnInProgress = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
double fraction = WiebeFraction(angleFromIgnition);
|
||||
Cylinder.InternalEnergy = preIgnitionInternalEnergy * (1.0 - fraction) + targetBurnEnergy * fraction;
|
||||
Cylinder.Mass = preIgnitionMass * (1.0 - fraction) + totalBurnMass * fraction;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Piston motion -----
|
||||
var (vol, dvdt) = PistonKinematics(cycleAngle);
|
||||
Cylinder.Volume = vol;
|
||||
Cylinder.Dvdt = dvdt;
|
||||
|
||||
// ----- Torque contribution -----
|
||||
double torque = ComputeTorque();
|
||||
crankshaft.AddTorque(torque);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,10 @@ namespace FluidSim.Components
|
||||
// Pre‑computed for damping
|
||||
private float _laminarCoeff; // 8*mu / r^2, then multiplied by DampingMultiplier
|
||||
|
||||
// ---- Energy loss (Newton cooling) ----
|
||||
private float _ambientEnergyReference; // total energy density at ambient (Pamb / (γ-1))
|
||||
public float EnergyRelaxationRate { get; set; } = 0.0f; // 1/s
|
||||
|
||||
public Pipe1D(double length, double area, int sampleRate, int forcedCellCount = 0)
|
||||
{
|
||||
float dtGlobal = 1f / sampleRate;
|
||||
@@ -89,12 +93,14 @@ namespace FluidSim.Components
|
||||
float radius = _diameter * 0.5f;
|
||||
_laminarCoeff = 8f * mu_air / (radius * radius); // will be multiplied by DampingMultiplier at each step
|
||||
|
||||
// Ambient reference energy (internal energy per unit volume at 101325 Pa)
|
||||
_ambientEnergyReference = 101325f / (_gamma - 1f); // ≈ 253312.5 J/m³
|
||||
|
||||
PortA = new Port();
|
||||
PortB = new Port();
|
||||
}
|
||||
|
||||
// ==================== PUBLIC API (unchanged) ============================
|
||||
|
||||
// ==================== PUBLIC API ============================
|
||||
public void SetABoundaryType(BoundaryType type) => _aBCType = type;
|
||||
public void SetBBoundaryType(BoundaryType type) => _bBCType = type;
|
||||
public void SetAAmbientPressure(double p) => _aAmbientPressure = (float)p;
|
||||
@@ -237,8 +243,6 @@ namespace FluidSim.Components
|
||||
// --- 2. Internal faces (1 .. n-1) – SIMD ---------------------------
|
||||
int vectorSize = Vector<float>.Count;
|
||||
int lastSimdFace = n - vectorSize; // highest face index that starts a full vector block
|
||||
// Face index f is between cell f-1 (left) and cell f (right).
|
||||
// We want to cover faces 1..n-1.
|
||||
for (int f = 1; f <= lastSimdFace; f += vectorSize)
|
||||
{
|
||||
SimdInternalFluxBlock(f, vectorSize);
|
||||
@@ -262,7 +266,7 @@ namespace FluidSim.Components
|
||||
float pR = PressureScalar(n - 1);
|
||||
ComputeRightBoundaryFlux(rhoR, uR, pR, out _fluxM[n], out _fluxP[n], out _fluxE[n]);
|
||||
|
||||
// --- 4. Cell update + damping – SIMD ------------------------------
|
||||
// --- 4. Cell update + damping + energy loss – SIMD -----------------
|
||||
SimdCellUpdate(dt);
|
||||
}
|
||||
|
||||
@@ -417,14 +421,9 @@ namespace FluidSim.Components
|
||||
// ==================== SIMD INTERNAL FACE ROUTINE ========================
|
||||
private void SimdInternalFluxBlock(int startFace, int count)
|
||||
{
|
||||
// startFace is the first face index; we process 'count' consecutive faces.
|
||||
// For face f, left cell = f-1, right cell = f.
|
||||
// We load left and right states for faces [startFace .. startFace+count-1].
|
||||
|
||||
int leftIdx = startFace - 1;
|
||||
int rightIdx = startFace;
|
||||
|
||||
// Load conserved variables for left cells and right cells as vectors.
|
||||
Vector<float> rL = new Vector<float>(_rho, leftIdx);
|
||||
Vector<float> ruL = new Vector<float>(_rhou, leftIdx);
|
||||
Vector<float> EL = new Vector<float>(_E, leftIdx);
|
||||
@@ -433,7 +432,6 @@ namespace FluidSim.Components
|
||||
Vector<float> ruR = new Vector<float>(_rhou, rightIdx);
|
||||
Vector<float> ER = new Vector<float>(_E, rightIdx);
|
||||
|
||||
// Derived quantities: u = ru / r, p = (gamma-1)*(E - 0.5*ru^2 / r)
|
||||
Vector<float> uL = ruL / rL;
|
||||
Vector<float> uR = ruR / rR;
|
||||
|
||||
@@ -444,35 +442,30 @@ namespace FluidSim.Components
|
||||
Vector<float> pL = gammaMinus1 * (EL - half * ruL * ruL / rL);
|
||||
Vector<float> pR = gammaMinus1 * (ER - half * ruR * ruR / rR);
|
||||
|
||||
// Sound speeds
|
||||
Vector<float> cL = Vector.SquareRoot(gammaVec * pL / rL);
|
||||
Vector<float> cR = Vector.SquareRoot(gammaVec * pR / rR);
|
||||
|
||||
// Wave speeds
|
||||
Vector<float> SL = Vector.Min(uL - cL, uR - cR);
|
||||
Vector<float> SR = Vector.Max(uL + cL, uR + cR);
|
||||
|
||||
// Star speed
|
||||
Vector<float> num = (pR - pL) + rL * uL * (SL - uL) - rR * uR * (SR - uR);
|
||||
Vector<float> den = rL * (SL - uL) - rR * (SR - uR);
|
||||
Vector<float> Ss = num / den;
|
||||
|
||||
// Total energy per unit mass (E/rho) for left/right (needed for star region)
|
||||
Vector<float> eL = EL / rL;
|
||||
Vector<float> eR = ER / rR;
|
||||
|
||||
// --- Compute all four possible flux vectors ---
|
||||
// Left flux
|
||||
Vector<float> Fm_L = ruL;
|
||||
Vector<float> Fp_L = ruL * uL + pL;
|
||||
Vector<float> Fe_L = (EL + pL) * uL; // (r*E + p)*u
|
||||
Vector<float> Fe_L = (EL + pL) * uL;
|
||||
|
||||
// Right flux
|
||||
Vector<float> Fm_R = ruR;
|
||||
Vector<float> Fp_R = ruR * uR + pR;
|
||||
Vector<float> Fe_R = (ER + pR) * uR;
|
||||
|
||||
// Star‑left fluxes (when SL < 0 < Ss)
|
||||
// Star‑left fluxes
|
||||
Vector<float> diffL = SL - uL;
|
||||
Vector<float> dL_den = SL - Ss;
|
||||
Vector<float> rsL = rL * diffL / dL_den;
|
||||
@@ -482,7 +475,7 @@ namespace FluidSim.Components
|
||||
Vector<float> Fp_starL = rsL * Ss * Ss + psSL;
|
||||
Vector<float> Fe_starL = (rsL * EsL + psSL) * Ss;
|
||||
|
||||
// Star‑right fluxes (when Ss < 0 < SR)
|
||||
// Star‑right fluxes
|
||||
Vector<float> diffR = SR - uR;
|
||||
Vector<float> dR_den = SR - Ss;
|
||||
Vector<float> rsR = rR * diffR / dR_den;
|
||||
@@ -492,14 +485,12 @@ namespace FluidSim.Components
|
||||
Vector<float> Fp_starR = rsR * Ss * Ss + psSR;
|
||||
Vector<float> Fe_starR = (rsR * EsR + psSR) * Ss;
|
||||
|
||||
// --- Select the correct flux based on wave signs ---
|
||||
var maskSLge0 = Vector.GreaterThanOrEqual(SL, Vector<float>.Zero);
|
||||
var maskSRle0 = Vector.LessThanOrEqual(SR, Vector<float>.Zero);
|
||||
var maskMiddle = ~(maskSLge0 | maskSRle0); // SL<0 && SR>0
|
||||
var maskMiddle = ~(maskSLge0 | maskSRle0);
|
||||
var maskStarL = maskMiddle & Vector.GreaterThanOrEqual(Ss, Vector<float>.Zero);
|
||||
var maskStarR = maskMiddle & Vector.LessThan(Ss, Vector<float>.Zero);
|
||||
|
||||
// Start with left flux, override with right/star as needed
|
||||
Vector<float> fm = Vector.ConditionalSelect(maskSLge0, Fm_L,
|
||||
Vector.ConditionalSelect(maskSRle0, Fm_R,
|
||||
Vector.ConditionalSelect(maskStarL, Fm_starL,
|
||||
@@ -515,13 +506,12 @@ namespace FluidSim.Components
|
||||
Vector.ConditionalSelect(maskStarL, Fe_starL,
|
||||
Vector.ConditionalSelect(maskStarR, Fe_starR, Vector<float>.Zero))));
|
||||
|
||||
// Store to flux arrays at indices startFace .. startFace+count-1
|
||||
fm.CopyTo(_fluxM, startFace);
|
||||
fp.CopyTo(_fluxP, startFace);
|
||||
fe.CopyTo(_fluxE, startFace);
|
||||
}
|
||||
|
||||
// ==================== SIMD CELL UPDATE + DAMPING ========================
|
||||
// ==================== SIMD CELL UPDATE + DAMPING + ENERGY LOSS =========
|
||||
private void SimdCellUpdate(float dt)
|
||||
{
|
||||
float dt_dx = dt / _dx;
|
||||
@@ -534,9 +524,11 @@ namespace FluidSim.Components
|
||||
int n = _n;
|
||||
int lastSimdCell = n - vectorSize;
|
||||
|
||||
// Pre‑define constants used in clamping
|
||||
// Pre‑defined constants used in clamping
|
||||
Vector<float> half = new Vector<float>(0.5f);
|
||||
Vector<float> gammaMinus1 = new Vector<float>(_gamma - 1f);
|
||||
Vector<float> ambientEnergyVec = new Vector<float>(_ambientEnergyReference);
|
||||
Vector<float> energyRelaxRateVec = new Vector<float>(EnergyRelaxationRate);
|
||||
|
||||
for (int i = 0; i <= lastSimdCell; i += vectorSize)
|
||||
{
|
||||
@@ -559,8 +551,12 @@ namespace FluidSim.Components
|
||||
Vector<float> newE = E - vDtDx * (flxE_R - flxE_L);
|
||||
|
||||
// Damping
|
||||
Vector<float> factor = Vector.Exp(-vCoeff / r * vDt);
|
||||
newRu *= factor;
|
||||
Vector<float> dampingFactor = Vector.Exp(-vCoeff / r * vDt);
|
||||
newRu *= dampingFactor;
|
||||
|
||||
// Energy loss (Newton cooling toward ambient)
|
||||
Vector<float> relaxFactor = Vector.Exp(-energyRelaxRateVec * vDt);
|
||||
newE = ambientEnergyVec + (newE - ambientEnergyVec) * relaxFactor;
|
||||
|
||||
// Clamp density
|
||||
newR = Vector.Max(newR, new Vector<float>(1e-12f));
|
||||
@@ -576,6 +572,7 @@ namespace FluidSim.Components
|
||||
}
|
||||
|
||||
// Scalar remainder
|
||||
float relaxRate = EnergyRelaxationRate;
|
||||
for (int i = Math.Max(0, lastSimdCell + 1); i < n; i++)
|
||||
{
|
||||
float r = _rho[i];
|
||||
@@ -584,15 +581,19 @@ namespace FluidSim.Components
|
||||
|
||||
float dM = _fluxM[i + 1] - _fluxM[i];
|
||||
float dP = _fluxP[i + 1] - _fluxP[i];
|
||||
float dE = _fluxE[i + 1] - _fluxE[i];
|
||||
float dE_flux = _fluxE[i + 1] - _fluxE[i];
|
||||
|
||||
float newR = r - dt_dx * dM;
|
||||
float newRu = ru - dt_dx * dP;
|
||||
float newE = E - dt_dx * dE;
|
||||
float newE = E - dt_dx * dE_flux;
|
||||
|
||||
// Damping
|
||||
float factor = MathF.Exp(-coeff / Math.Max(r, 1e-12f) * dt);
|
||||
newRu *= factor;
|
||||
float dampingFactor = MathF.Exp(-coeff / Math.Max(r, 1e-12f) * dt);
|
||||
newRu *= dampingFactor;
|
||||
|
||||
// Energy loss
|
||||
float relaxFactor = MathF.Exp(-relaxRate * dt);
|
||||
newE = _ambientEnergyReference + (newE - _ambientEnergyReference) * relaxFactor;
|
||||
|
||||
// Clamps
|
||||
if (newR < 1e-12f) newR = 1e-12f;
|
||||
|
||||
Reference in New Issue
Block a user