Face the Quaternius Superhero Female toward a fixed front 3/4 camera during the auto capture pass, note the free-pack bald base mesh, and check in the updated Lavapipe PNG sequence. Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>
285 lines
9.8 KiB
Plaintext
285 lines
9.8 KiB
Plaintext
// Venom-style body coverage demo (SakuraRabbit / Gwen-transform inspired).
|
|
// Quaternius CC0 female character + height/noise goo shader that spreads
|
|
// feet -> head with an emissive frontier and decorative tendril ribbons.
|
|
// Space = play/pause | Left/Right = scrub | R = reset | P = screenshot
|
|
|
|
persist venomShader = null
|
|
persist venomCamera = null
|
|
persist venomModel = null
|
|
persist venomParts = []
|
|
persist venomTendrils = []
|
|
persist venomDroplets = []
|
|
persist venomGround = null
|
|
persist venomCoverage = 0.0
|
|
persist venomPlaying = true
|
|
persist venomTime = 0.0
|
|
persist venomYaw = 0.72
|
|
persist venomFrame = 0
|
|
persist venomCaptureIdx = 0
|
|
persist venomCapturePending = false
|
|
persist venomCaptureName = ""
|
|
persist venomAutoMode = true
|
|
persist venomAutoHold = 0.0
|
|
persist venomDone = false
|
|
|
|
venomAutoShots <- [0.0, 0.28, 0.55, 0.82, 1.0]
|
|
venomCharacterHeight <- 1.80
|
|
// Quaternius free Universal Base is bald (full hair is in the paid pack);
|
|
// eyebrows/eyes still ship as separate meshes with the hair/eye textures.
|
|
|
|
function keyJust(k) {
|
|
return key_just_pressed(k);
|
|
}
|
|
|
|
function pushUniforms() {
|
|
venomShader.sendFloat("coverage", venomCoverage);
|
|
venomShader.sendFloat("edgeWidth", 0.06);
|
|
venomShader.sendFloat("noiseScale", 2.2);
|
|
venomShader.sendFloat("time", venomTime);
|
|
venomShader.sendFloat("gooR", 0.02);
|
|
venomShader.sendFloat("gooG", 0.02);
|
|
venomShader.sendFloat("gooB", 0.04);
|
|
venomShader.sendFloat("edgeR", 0.55);
|
|
venomShader.sendFloat("edgeG", 0.12);
|
|
venomShader.sendFloat("edgeB", 0.85);
|
|
venomShader.sendFloat("edgeGlow", 2.4);
|
|
venomShader.sendFloat("gloss", 0.92);
|
|
}
|
|
|
|
function addTendril(seed) {
|
|
local t = {
|
|
body = eve.Renderable3D()
|
|
seed = seed.tofloat()
|
|
phase = seed * 0.73
|
|
};
|
|
t.body.setMesh(gfx.newMeshCylinder(10, 1, true));
|
|
t.body.setShader(venomShader);
|
|
t.body.setTint(0.03, 0.03, 0.05, 1.0);
|
|
t.body.setRoughness(0.2);
|
|
t.body.setCastShadow(false);
|
|
t.body.setReceiveShadow(true);
|
|
venomTendrils.push(t);
|
|
}
|
|
|
|
function addDroplet(seed) {
|
|
local d = {
|
|
body = eve.Renderable3D()
|
|
seed = seed.tofloat()
|
|
life = 0.0
|
|
};
|
|
d.body.setMesh(gfx.newMeshSphere(8, 6));
|
|
d.body.setShader(venomShader);
|
|
d.body.setTint(0.04, 0.04, 0.06, 1.0);
|
|
d.body.setRoughness(0.15);
|
|
d.body.setCastShadow(false);
|
|
venomDroplets.push(d);
|
|
}
|
|
|
|
function buildCharacter() {
|
|
if (model3d == null) model3d = eve.Model3D();
|
|
venomModel = model3d.newModelDataFromFile(
|
|
"assets/quaternius/Superhero_Female_FullBody.gltf");
|
|
if (venomModel == null)
|
|
throw "Failed to load Quaternius Superhero_Female_FullBody.gltf";
|
|
|
|
local meshCount = venomModel.getMeshCount();
|
|
print(format("[venom] loaded character meshes=%d\n", meshCount));
|
|
for (local i = 0; i < meshCount; ++i) {
|
|
local part = model3d.createRenderable(gfx, venomModel, i);
|
|
part.setPosition(0.0, 0.0, 0.0);
|
|
// Default bind facing is +Z; orbit camera sits on +Z at yaw=0.
|
|
part.setYaw(0.0);
|
|
part.setCastShadow(true);
|
|
part.setReceiveShadow(true);
|
|
part.setReceiveLight(true);
|
|
local mat = part.getMaterial();
|
|
mat.setShader(venomShader);
|
|
mat.setShadingModel("custom");
|
|
mat.setRoughness(0.55);
|
|
// Keep authored albedo/normal if the importer bound them.
|
|
local texPath = venomModel.getMaterialTexturePath(
|
|
venomModel.getMaterialIndex(i), "base_color", 0);
|
|
if (texPath != "") {
|
|
local tex = gfx.newTextureFromFile("assets/quaternius/" + texPath);
|
|
part.setTexture(tex);
|
|
mat.setAlbedoTexture(tex);
|
|
}
|
|
venomParts.push(part);
|
|
}
|
|
if (venomParts.len() == 0)
|
|
throw "Character had no renderable meshes";
|
|
|
|
for (local i = 0; i < 10; ++i) addTendril(i);
|
|
for (local i = 0; i < 14; ++i) addDroplet(i);
|
|
|
|
venomGround = eve.Renderable3D();
|
|
venomGround.setMesh(gfx.newMeshCube(1.0));
|
|
venomGround.setPosition(0.0, -0.04, 0.0);
|
|
venomGround.setScale(5.5, 0.08, 5.5);
|
|
venomGround.setTint(0.55, 0.16, 0.22, 1.0);
|
|
venomGround.setRoughness(0.95);
|
|
venomGround.setCastShadow(false);
|
|
venomGround.setReceiveShadow(true);
|
|
}
|
|
|
|
function updateTendrils(_dt) {
|
|
local frontY = venomCoverage * venomCharacterHeight;
|
|
local bloom = 1.0 - fabs(venomCoverage - 0.55) / 0.55;
|
|
if (bloom < 0.0) bloom = 0.0;
|
|
bloom = bloom * bloom;
|
|
|
|
for (local i = 0; i < venomTendrils.len(); ++i) {
|
|
local t = venomTendrils[i];
|
|
local ang = t.phase + venomTime * (0.7 + t.seed * 0.05) + i * 0.628;
|
|
local radius = 0.28 + 0.22 * bloom + 0.05 * sin(venomTime * 2.1 + t.seed);
|
|
local y = frontY + 0.06 * sin(venomTime * 3.0 + t.seed);
|
|
local len = (0.12 + 0.45 * bloom) * (0.7 + 0.3 * sin(venomTime * 1.7 + i));
|
|
local thick = bloom > 0.05 ? (0.015 + 0.025 * bloom) : 0.001;
|
|
local visibleLen = bloom > 0.05 ? len : 0.001;
|
|
local pitch = 0.45 + 0.55 * sin(venomTime * 2.4 + t.seed);
|
|
t.body.setPosition(cos(ang) * radius, y, sin(ang) * radius * 0.7);
|
|
t.body.setScale(thick, visibleLen, thick);
|
|
t.body.setRotation(ang, pitch, 0.0);
|
|
}
|
|
}
|
|
|
|
function updateDroplets(dt) {
|
|
local frontY = venomCoverage * venomCharacterHeight;
|
|
local active = venomCoverage > 0.05 && venomCoverage < 0.95;
|
|
for (local i = 0; i < venomDroplets.len(); ++i) {
|
|
local d = venomDroplets[i];
|
|
d.life += dt * (0.8 + (i % 5) * 0.13);
|
|
if (d.life > 1.0) d.life -= 1.0;
|
|
local ang = d.seed * 1.7 + venomTime * 0.4;
|
|
local spread = 0.20 + d.life * 0.50;
|
|
local y = frontY + 0.22 * d.life;
|
|
local size = active ? (0.012 + 0.025 * (1.0 - d.life)) : 0.001;
|
|
d.body.setPosition(cos(ang) * spread, y, sin(ang) * spread * 0.7);
|
|
d.body.setScale(size, size * 1.4, size);
|
|
}
|
|
}
|
|
|
|
function orbitCamera() {
|
|
// Front 3/4 of the full body so the goo climb reads clearly.
|
|
local radius = 3.9;
|
|
local eyeY = 1.35;
|
|
venomCamera.setEye(sin(venomYaw) * radius, eyeY, cos(venomYaw) * radius);
|
|
venomCamera.setTarget(0.0, 0.90, 0.0);
|
|
}
|
|
|
|
function requestShot(name) {
|
|
venomCaptureName = name;
|
|
venomCapturePending = true;
|
|
}
|
|
|
|
eve_init = function() {
|
|
gfx.setBackgroundColor(0.62, 0.18, 0.28, 1.0);
|
|
if (venomShader == null) {
|
|
venomShader = gfx.newMeshShader(fs.readText("shaders/venom.frag"));
|
|
local names = ["coverage", "edgeWidth", "noiseScale", "time",
|
|
"gooR", "gooG", "gooB", "edgeR", "edgeG", "edgeB",
|
|
"edgeGlow", "gloss"];
|
|
foreach (n in names) venomShader.declareFloat(n);
|
|
}
|
|
|
|
venomCamera = eve.Camera3D();
|
|
venomCamera.setUp(0.0, 1.0, 0.0);
|
|
venomCamera.setFov(38.0);
|
|
venomCamera.setAmbient(0.28, 0.24, 0.30);
|
|
venomCamera.setActive(true);
|
|
gfx.setDirectionalLight(-0.35, 0.90, 0.35, 1.65, 1.40, 1.25);
|
|
|
|
local rc = gfx.getRenderControl();
|
|
rc.disable("ao");
|
|
rc.disable("gi");
|
|
rc.enable("msaa");
|
|
rc.compile();
|
|
|
|
if (venomParts.len() == 0) buildCharacter();
|
|
orbitCamera();
|
|
pushUniforms();
|
|
print("[venom] Quaternius Superhero Female (CC0)\n");
|
|
print("[venom] Space play/pause | Left/Right scrub | R reset | P screenshot\n");
|
|
print("[venom] auto demo will capture coverage steps then exit\n");
|
|
};
|
|
|
|
eve_update = function(dt) {
|
|
venomFrame += 1;
|
|
venomTime += dt;
|
|
// Keep a fixed front 3/4 during the auto capture pass so every
|
|
// coverage step shows the character face-on; orbit only when interactive.
|
|
if (!venomAutoMode)
|
|
venomYaw += dt * 0.22;
|
|
orbitCamera();
|
|
|
|
if (keyJust("space")) venomPlaying = !venomPlaying;
|
|
if (keyJust("r") || keyJust("R")) {
|
|
venomCoverage = 0.0;
|
|
venomPlaying = true;
|
|
venomAutoMode = false;
|
|
}
|
|
if (keyJust("left")) {
|
|
venomCoverage = max(0.0, venomCoverage - 0.05);
|
|
venomPlaying = false;
|
|
venomAutoMode = false;
|
|
}
|
|
if (keyJust("right")) {
|
|
venomCoverage = min(1.0, venomCoverage + 0.05);
|
|
venomPlaying = false;
|
|
venomAutoMode = false;
|
|
}
|
|
if (keyJust("p") || keyJust("P"))
|
|
requestShot(format("captures/venom-manual-%.0f.png", venomCoverage * 100.0));
|
|
|
|
if (venomAutoMode) {
|
|
if (venomCaptureIdx < venomAutoShots.len()) {
|
|
local target = venomAutoShots[venomCaptureIdx];
|
|
local diff = target - venomCoverage;
|
|
if (fabs(diff) > 0.01) {
|
|
local step = (diff > 0.0 ? 1.0 : -1.0) * dt * 0.45;
|
|
if (fabs(step) > fabs(diff)) step = diff;
|
|
venomCoverage += step;
|
|
venomAutoHold = 0.0;
|
|
} else {
|
|
venomCoverage = target;
|
|
venomAutoHold += dt;
|
|
if (venomAutoHold > 0.40 && !venomCapturePending) {
|
|
requestShot(format("captures/venom-%02d-c%.0f.png",
|
|
venomCaptureIdx, target * 100.0));
|
|
}
|
|
}
|
|
} else if (!venomDone && !venomCapturePending) {
|
|
venomDone = true;
|
|
print("[venom] auto demo complete\n");
|
|
// Prefer load.nut bootBench early-exit so the window is not
|
|
// destroyed mid-frame (win.close() here segfaults under Vulkan).
|
|
eve.bootBench = true;
|
|
return;
|
|
}
|
|
} else if (venomPlaying) {
|
|
venomCoverage += dt * 0.20;
|
|
if (venomCoverage >= 1.0) {
|
|
venomCoverage = 1.0;
|
|
venomPlaying = false;
|
|
}
|
|
}
|
|
|
|
updateTendrils(dt);
|
|
updateDroplets(dt);
|
|
pushUniforms();
|
|
};
|
|
|
|
eve_render = function() {
|
|
if (venomCapturePending && venomFrame > 8 &&
|
|
gfx.saveFramePng(venomCaptureName)) {
|
|
print("[venom] saved " + venomCaptureName + "\n");
|
|
venomCapturePending = false;
|
|
if (venomAutoMode) {
|
|
venomCaptureIdx += 1;
|
|
venomAutoHold = 0.0;
|
|
}
|
|
}
|
|
gfx.clear();
|
|
gfx.render3D();
|
|
};
|