327 lines
12 KiB
JavaScript
327 lines
12 KiB
JavaScript
const GLSL = `
|
|
const float PLANETARY_TAU = 6.283185307179586;
|
|
|
|
float planetaryRoundedSunTeeth() {
|
|
return clamp(floor(sunTeeth + 0.5), 12.0, 30.0);
|
|
}
|
|
|
|
float planetaryPlanetTeeth() {
|
|
return max(floor(planetTeeth + 0.5), 3.0);
|
|
}
|
|
|
|
float planetarySunTeeth() {
|
|
float sun = planetaryRoundedSunTeeth();
|
|
float planet = planetaryPlanetTeeth();
|
|
float remainder = mod(sun + planet, 3.0);
|
|
float adjusted = sun;
|
|
if (remainder > 0.5 && remainder < 1.5) {
|
|
adjusted -= 1.0;
|
|
} else if (remainder >= 1.5) {
|
|
adjusted += 1.0;
|
|
}
|
|
if (adjusted < 12.0) {
|
|
adjusted += 3.0;
|
|
}
|
|
if (adjusted > 30.0) {
|
|
adjusted -= 3.0;
|
|
}
|
|
return max(adjusted, 3.0);
|
|
}
|
|
|
|
float planetaryRingTeeth() {
|
|
return planetarySunTeeth() + planetaryPlanetTeeth() * 2.0;
|
|
}
|
|
|
|
float planetarySunPitch() {
|
|
return gearModule * planetarySunTeeth() * 0.5;
|
|
}
|
|
|
|
float planetaryPlanetPitch() {
|
|
return gearModule * planetaryPlanetTeeth() * 0.5;
|
|
}
|
|
|
|
float planetaryMeshSpread() {
|
|
return max(meshOffset, 0.0);
|
|
}
|
|
|
|
float planetaryRingPitch() {
|
|
return gearModule * planetaryRingTeeth() * 0.5 + planetaryMeshSpread();
|
|
}
|
|
|
|
float planetaryDriveRadians() {
|
|
return meshPhase * 0.017453292519943295;
|
|
}
|
|
|
|
float planetaryCarrierRadians() {
|
|
return carrierAngle * 0.017453292519943295;
|
|
}
|
|
|
|
float planetaryDrivenRingRadians() {
|
|
float sun = planetarySunTeeth();
|
|
float ring = planetaryRingTeeth();
|
|
float carrier = planetaryCarrierRadians();
|
|
return carrier - (sun / max(ring, 1.0)) * (planetaryDriveRadians() - carrier);
|
|
}
|
|
|
|
float planetaryRingRadians() {
|
|
float oddPlanetOffset = mod(planetaryPlanetTeeth(), 2.0) * PLANETARY_TAU * 0.5 / max(planetaryRingTeeth(), 1.0);
|
|
return ringPhase * 0.017453292519943295 + planetaryDrivenRingRadians() + oddPlanetOffset;
|
|
}
|
|
|
|
float planetaryToothWave(float angle, float teeth, float phase) {
|
|
float wave = 0.5 + 0.5 * cos((angle - phase) * teeth);
|
|
return wave * wave * (3.0 - 2.0 * wave);
|
|
}
|
|
|
|
float planetaryExtrude(float d2, float z, float halfHeight, float bevel) {
|
|
vec2 q = vec2(d2, abs(z) - halfHeight + bevel);
|
|
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - bevel;
|
|
}
|
|
|
|
float planetaryExternalGear2(vec2 p, float pitchRadius, float teeth, float phase) {
|
|
float theta = atan(p.y, p.x);
|
|
float tooth = planetaryToothWave(theta, teeth, phase);
|
|
float root = toothDepth * 0.72;
|
|
float addendum = toothDepth * 0.82;
|
|
float meshClearance = max(backlash, max(gearModule * 0.05, toothDepth * 0.18));
|
|
float boundary = pitchRadius - root + tooth * (root + addendum) - meshClearance * 0.52;
|
|
return length(p) - boundary;
|
|
}
|
|
|
|
float planetaryInternalRing2(vec2 p, float pitchRadius, float teeth, float phase) {
|
|
float theta = atan(p.y, p.x);
|
|
float tooth = planetaryToothWave(theta, teeth, phase);
|
|
float root = toothDepth * 0.78;
|
|
float addendum = toothDepth * 0.9;
|
|
float meshClearance = max(backlash, max(gearModule * 0.05, toothDepth * 0.18));
|
|
float innerBoundary = pitchRadius + root - tooth * (root + addendum) + meshClearance * 0.86;
|
|
float outerBoundary = pitchRadius + toothDepth * 2.55 + gearModule * 3.0;
|
|
return max(innerBoundary - length(p), length(p) - outerBoundary);
|
|
}
|
|
|
|
float planetaryExternalGear(vec3 p, vec2 center, float pitchRadius, float teeth, float phase) {
|
|
float gear = planetaryExternalGear2(p.xy - center, pitchRadius, teeth, phase);
|
|
return planetaryExtrude(gear, p.z, thickness * 0.5, min(gearModule * 0.16, 0.38));
|
|
}
|
|
|
|
float planetaryRingGear(vec3 p) {
|
|
float ring = planetaryInternalRing2(p.xy, planetaryRingPitch(), planetaryRingTeeth(), planetaryRingRadians());
|
|
return planetaryExtrude(ring, p.z, thickness * 0.5, min(gearModule * 0.14, 0.34));
|
|
}
|
|
|
|
float planetarySunGear(vec3 p) {
|
|
float gear = planetaryExternalGear(p, vec2(0.0), planetarySunPitch(), planetarySunTeeth(), planetaryDriveRadians());
|
|
float bore = implicit_cylinder_capped(
|
|
p,
|
|
vec3(0.0, 0.0, -thickness),
|
|
vec3(0.0, 0.0, thickness),
|
|
max(gearModule * 1.15, planetarySunPitch() * 0.19)
|
|
);
|
|
float hub = planetaryExtrude(length(p.xy) - planetarySunPitch() * 0.42, p.z, thickness * 0.58, min(gearModule * 0.18, 0.42));
|
|
return implicit_intersect_round(implicit_union_round(gear, hub, gearModule * 0.18), -bore, gearModule * 0.12);
|
|
}
|
|
|
|
vec2 planetaryPlanetCenter(float index) {
|
|
float angle = planetaryCarrierRadians() + index * PLANETARY_TAU / 3.0;
|
|
float orbit = planetarySunPitch() + planetaryPlanetPitch() + planetaryMeshSpread();
|
|
return orbit * vec2(cos(angle), sin(angle));
|
|
}
|
|
|
|
float planetaryPlanetPhase(float index) {
|
|
float planet = planetaryPlanetTeeth();
|
|
float centerAngle = planetaryCarrierRadians() + index * PLANETARY_TAU / 3.0;
|
|
float sunRatio = planetarySunTeeth() / max(planet, 1.0);
|
|
float carrierRatio = (planetarySunTeeth() + planet) / max(planet, 1.0);
|
|
float toothOffset = PLANETARY_TAU * 0.5 - PLANETARY_TAU * 0.5 / max(planet, 1.0);
|
|
return centerAngle * carrierRatio - planetaryDriveRadians() * sunRatio + toothOffset;
|
|
}
|
|
|
|
float planetaryPlanetGear(vec3 p, float index) {
|
|
vec2 center = planetaryPlanetCenter(index);
|
|
float gear = planetaryExternalGear(p, center, planetaryPlanetPitch(), planetaryPlanetTeeth(), planetaryPlanetPhase(index));
|
|
float bore = implicit_cylinder_capped(
|
|
p,
|
|
vec3(center, -thickness),
|
|
vec3(center, thickness),
|
|
max(gearModule * 0.86, planetaryPlanetPitch() * 0.2)
|
|
);
|
|
float pinBoss = planetaryExtrude(length(p.xy - center) - max(gearModule * 1.35, planetaryPlanetPitch() * 0.34), p.z, thickness * 0.54, min(gearModule * 0.16, 0.36));
|
|
return implicit_intersect_round(implicit_union_round(gear, pinBoss, gearModule * 0.15), -bore, gearModule * 0.1);
|
|
}
|
|
|
|
float planetaryCarrierArm(vec3 p, vec2 center) {
|
|
float zCenter = -thickness * 0.72;
|
|
float arm2 = implicit_line_segment2(p.xy, vec2(0.0), center) - gearModule * 0.95;
|
|
return planetaryExtrude(arm2, p.z - zCenter, thickness * 0.16, min(gearModule * 0.16, 0.35));
|
|
}
|
|
|
|
float planetaryCarrier(vec3 p) {
|
|
float hub = planetaryExtrude(length(p.xy) - max(gearModule * 2.1, planetarySunPitch() * 0.34), p.z + thickness * 0.72, thickness * 0.17, min(gearModule * 0.16, 0.35));
|
|
float carrier = hub;
|
|
for (int i = 0; i < 3; i += 1) {
|
|
vec2 center = planetaryPlanetCenter(float(i));
|
|
float arm = planetaryCarrierArm(p, center);
|
|
float pin = planetaryExtrude(length(p.xy - center) - max(gearModule * 1.05, planetaryPlanetPitch() * 0.26), p.z + thickness * 0.72, thickness * 0.33, min(gearModule * 0.16, 0.35));
|
|
carrier = implicit_union_round(carrier, arm, gearModule * 0.32);
|
|
carrier = implicit_union_round(carrier, pin, gearModule * 0.2);
|
|
}
|
|
return carrier;
|
|
}
|
|
|
|
float sdf(vec3 p) {
|
|
float result = planetaryRingGear(p);
|
|
result = min(result, planetarySunGear(p));
|
|
for (int i = 0; i < 3; i += 1) {
|
|
result = min(result, planetaryPlanetGear(p, float(i)));
|
|
}
|
|
if (showCarrier) {
|
|
result = min(result, planetaryCarrier(p));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
vec3 planetaryPalette(float component, vec3 p, vec3 normal) {
|
|
vec3 color = ringColor;
|
|
if (component > 0.5) {
|
|
color = sunColor;
|
|
}
|
|
if (component >= 1.5) {
|
|
color = planetAColor;
|
|
}
|
|
if (component >= 2.33) {
|
|
color = planetBColor;
|
|
}
|
|
if (component >= 2.66) {
|
|
color = planetCColor;
|
|
}
|
|
if (component >= 3.5) {
|
|
color = carrierColor;
|
|
}
|
|
float toothLine = 0.5 + 0.5 * sin(atan(p.y, p.x) * planetaryRingTeeth());
|
|
float faceLight = smoothstep(0.22, 1.0, abs(normal.z));
|
|
color *= 0.92 + 0.15 * toothLine;
|
|
color = mix(color * 0.72, min(color * 1.18 + vec3(0.05), vec3(1.0)), faceLight);
|
|
return clamp(color, vec3(0.0), vec3(1.0));
|
|
}
|
|
|
|
vec3 color(vec3 p, vec3 normal) {
|
|
float best = planetaryRingGear(p);
|
|
float component = 0.0;
|
|
|
|
float sun = planetarySunGear(p);
|
|
if (sun < best) {
|
|
best = sun;
|
|
component = 1.0;
|
|
}
|
|
|
|
for (int i = 0; i < 3; i += 1) {
|
|
float planet = planetaryPlanetGear(p, float(i));
|
|
if (planet < best) {
|
|
best = planet;
|
|
component = 2.0 + float(i) * 0.34;
|
|
}
|
|
}
|
|
|
|
if (showCarrier) {
|
|
float carrier = planetaryCarrier(p);
|
|
if (carrier < best) {
|
|
component = 4.0;
|
|
}
|
|
}
|
|
|
|
return planetaryPalette(component, p, normal);
|
|
}
|
|
`;
|
|
|
|
const PLANETARY_MIN_SUN_TEETH = 12;
|
|
const PLANETARY_MAX_SUN_TEETH = 30;
|
|
|
|
function roundedTeeth(value, fallback) {
|
|
const numeric = Number(value);
|
|
return Math.round(Number.isFinite(numeric) ? numeric : fallback);
|
|
}
|
|
|
|
function compatibleSunTeeth(params) {
|
|
const planetTeeth = Math.max(3, roundedTeeth(params.planetTeeth, 12));
|
|
const rawSunTeeth = Math.min(
|
|
Math.max(roundedTeeth(params.sunTeeth, 18), PLANETARY_MIN_SUN_TEETH),
|
|
PLANETARY_MAX_SUN_TEETH
|
|
);
|
|
const candidates = [rawSunTeeth - 1, rawSunTeeth, rawSunTeeth + 1, rawSunTeeth - 2, rawSunTeeth + 2]
|
|
.filter((candidate) => candidate >= PLANETARY_MIN_SUN_TEETH && candidate <= PLANETARY_MAX_SUN_TEETH)
|
|
.filter((candidate) => (candidate + planetTeeth) % 3 === 0);
|
|
return candidates.length ? candidates[0] : rawSunTeeth;
|
|
}
|
|
|
|
export default {
|
|
schema: "implicit.js/0.1.0",
|
|
name: "planetary gear",
|
|
description: "A shader-parametric planetary gear set with a sun gear, three planet gears, internal ring teeth, and an under-carrier.",
|
|
units: "mm",
|
|
params: {
|
|
sunTeeth: { type: "number", label: "Sun teeth", min: 12, max: 30, step: 1, default: 18 },
|
|
planetTeeth: { type: "number", label: "Planet teeth", min: 8, max: 18, step: 1, default: 12 },
|
|
gearModule: { type: "number", label: "Gear module", min: 1.2, max: 3.2, step: 0.05, default: 2.0, unit: "mm" },
|
|
toothDepth: { type: "number", label: "Tooth depth", min: 0.6, max: 3.6, step: 0.05, default: 1.55, unit: "mm" },
|
|
thickness: { type: "number", label: "Gear thickness", min: 3, max: 12, step: 0.25, default: 6.5, unit: "mm" },
|
|
backlash: { type: "number", label: "Backlash", min: 0, max: 1.4, step: 0.02, default: 0.22, unit: "mm" },
|
|
meshOffset: { type: "number", label: "Mesh spread", min: 0, max: 5, step: 0.05, default: 0, unit: "mm" },
|
|
meshPhase: { type: "number", label: "Drive phase", min: 0, max: 360, step: 1, default: 16, unit: "deg" },
|
|
carrierAngle: { type: "number", label: "Carrier orbit", min: -180, max: 180, step: 1, default: 0, unit: "deg" },
|
|
ringPhase: { type: "number", label: "Ring phase", min: -180, max: 180, step: 1, default: 0, unit: "deg" },
|
|
showCarrier: { type: "boolean", label: "Show carrier", default: true },
|
|
ringColor: { type: "color", label: "Ring color", default: "#00a7ff" },
|
|
sunColor: { type: "color", label: "Sun color", default: "#ffe83d" },
|
|
planetAColor: { type: "color", label: "Planet A", default: "#ff5b2e" },
|
|
planetBColor: { type: "color", label: "Planet B", default: "#d94dff" },
|
|
planetCColor: { type: "color", label: "Planet C", default: "#7cff4f" },
|
|
carrierColor: { type: "color", label: "Carrier color", default: "#8b6cff" }
|
|
},
|
|
animations: {
|
|
meshCycle: {
|
|
label: "Mesh cycle",
|
|
duration: 8,
|
|
loop: true,
|
|
update({ progress, params, set }) {
|
|
const drive = progress * 360;
|
|
const sunTeeth = compatibleSunTeeth(params);
|
|
const planetTeeth = Math.max(3, roundedTeeth(params.planetTeeth, 12));
|
|
const ringTeeth = sunTeeth + planetTeeth * 2;
|
|
const carrier = drive * sunTeeth / Math.max(sunTeeth + ringTeeth, 1);
|
|
set("meshPhase", drive);
|
|
set("carrierAngle", ((carrier + 180) % 360) - 180);
|
|
set("ringPhase", 0);
|
|
}
|
|
},
|
|
explodeMesh: {
|
|
label: "Explode mesh",
|
|
duration: 5.5,
|
|
loop: true,
|
|
update({ progress, set }) {
|
|
const wave = 0.5 - 0.5 * Math.cos(progress * Math.PI * 2);
|
|
set("meshOffset", wave * 4.2);
|
|
set("carrierAngle", -28 + wave * 56);
|
|
}
|
|
}
|
|
},
|
|
bounds: ({ params }) => {
|
|
const sunTeeth = compatibleSunTeeth(params);
|
|
const planetTeeth = Math.max(3, roundedTeeth(params.planetTeeth, 12));
|
|
const ringTeeth = sunTeeth + planetTeeth * 2;
|
|
const spread = Math.max(params.meshOffset, 0);
|
|
const ringPitch = params.gearModule * ringTeeth * 0.5 + spread;
|
|
const ringOuter = ringPitch + params.toothDepth * 2.8 + params.gearModule * 3.4 + 2;
|
|
const z = params.thickness * 0.92 + 5;
|
|
return [[-ringOuter, -ringOuter, -z], [ringOuter, ringOuter, z]];
|
|
},
|
|
render: ({ params }) => ({
|
|
steps: 320,
|
|
epsilon: Math.max(0.005, params.gearModule * 0.0025),
|
|
normalEpsilon: Math.max(0.038, params.gearModule * 0.018),
|
|
stepScale: 0.5,
|
|
maxStep: Math.max(params.gearModule * 0.62, 0.65)
|
|
}),
|
|
glsl: GLSL
|
|
};
|