Replace venom demo mannequin with Quaternius CC0 character.

Load Superhero Female FullBody via model3d, sample albedo in the coverage
shader, and document the asset license under examples/venom-transform.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>
This commit is contained in:
Cursor Agentand西风逍遥游 committed 2026-09-11 12:00:32 +00:00
1 parent 92736e1656
commit 5cc2e98313
15 files changed
+2183 -83

No files matched your search

+14 -6
View File
@@ -2,8 +2,8 @@
Interactive approximation of the Bilibili / Unity "毒液变装" look
([BV12H4y1D7qf](https://www.bilibili.com/video/BV12H4y1D7qf)): glossy black
symbiote coverage crawling up a mannequin with an emissive frontier, tendril
ribbons, and splash droplets.
symbiote coverage crawling up a textured character with an emissive frontier,
tendril ribbons, and splash droplets.
## What this proves
@@ -16,12 +16,19 @@ EVEngine can assemble that effect from existing pieces:
| Frontier glow | Magenta/purple emissive band along the coverage edge |
| Tendrils | Animated thin cylinders around the active frontier |
| Droplets | Small spheres emitted near the front |
| Host mesh | Primitive mannequin (sphere / cylinder), same path as any `Renderable3D` |
| Host mesh | Quaternius Superhero Female (CC0) via `model3d` + custom material shader |
This is intentionally a **shader + props** demo, not a volumetric metaball
solver. That matches the Unity reference technique more closely than a full
fluid simulation would.
## Character asset
`assets/quaternius/` vendors the CC0 Quaternius Universal Base Characters
female FullBody glTF (same pack as `examples/character-motion-lab`). See
`assets/quaternius/LICENSE-CHARACTERS.txt` and
[quaternius.com](https://quaternius.com).
## Run
```bash
@@ -53,9 +60,10 @@ On launch the demo auto-plays coverage `0 → 1`, writes PNGs under
## Files
- `main.nut` — scene, mannequin, tendrils, auto-capture
- `shaders/venom.frag` — coverage / goo / edge shader
- `config.nut` — window + modules
- `main.nut` — scene, character load, tendrils, auto-capture
- `shaders/venom.frag` — coverage / goo / edge shader (samples albedo)
- `config.nut` — window + modules (`model3d`)
- `assets/quaternius/` — CC0 character glTF + textures
- `captures/` — PNG outputs from the auto demo
## Captures
@@ -0,0 +1,22 @@
This is the standard FREE version of the Universal Base Characters Kit, which only contains
a portion of the models . You can buy the SOURCE version which has all the models with rigged .blends,
and Unity(URP), Unreal Engine and Godot projects
with custom shaders already set up.
You can get the other version from the website https://quaternius.com
-------------------------------------------------------
License:
CC0 1.0 Universal (CC0 1.0)
Public Domain Dedication
https://creativecommons.org/publicdomain/zero/1.0/
------------------------------------------------------
Models by @Quaternius
Consider supporting me on Patreon!
https://www.patreon.com/quaternius
-------------------------------------------------------
Join the Discord Server:
https://discord.gg/vJqnRUYRfT
@@ -0,0 +1,5 @@
# Quaternius Universal Base Characters (CC0)
Female FullBody model copied from `examples/character-motion-lab/assets/quaternius`
for the venom-transform demo. Original pack: Universal Base Characters Standard
(https://quaternius.com). License: CC0 1.0 (see LICENSE-CHARACTERS.txt).
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

+1 -1
View File
@@ -4,5 +4,5 @@ config = {
title = "EVEngine — Venom Transform Demo"
hotReload = true
debug = false
modules = ["gfx", "win", "fs"]
modules = ["gfx", "win", "fs", "model3d"]
};
+67 -66
View File
@@ -1,10 +1,11 @@
// Venom-style body coverage demo (SakuraRabbit / Gwen-transform inspired).
// Height + noise shader spreads glossy black goo feet -> head, with an
// emissive frontier and decorative tendril ribbons.
// 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 = []
@@ -12,7 +13,7 @@ persist venomGround = null
persist venomCoverage = 0.0
persist venomPlaying = true
persist venomTime = 0.0
persist venomYaw = 0.35
persist venomYaw = 0.55
persist venomFrame = 0
persist venomCaptureIdx = 0
persist venomCapturePending = false
@@ -22,6 +23,7 @@ persist venomAutoHold = 0.0
persist venomDone = false
venomAutoShots <- [0.0, 0.28, 0.55, 0.82, 1.0]
venomCharacterHeight <- 1.80
function keyJust(k) {
return key_just_pressed(k);
@@ -29,8 +31,8 @@ function keyJust(k) {
function pushUniforms() {
venomShader.sendFloat("coverage", venomCoverage);
venomShader.sendFloat("edgeWidth", 0.07);
venomShader.sendFloat("noiseScale", 2.4);
venomShader.sendFloat("edgeWidth", 0.06);
venomShader.sendFloat("noiseScale", 2.2);
venomShader.sendFloat("time", venomTime);
venomShader.sendFloat("gooR", 0.02);
venomShader.sendFloat("gooG", 0.02);
@@ -38,24 +40,10 @@ function pushUniforms() {
venomShader.sendFloat("edgeR", 0.55);
venomShader.sendFloat("edgeG", 0.12);
venomShader.sendFloat("edgeB", 0.85);
venomShader.sendFloat("edgeGlow", 2.8);
venomShader.sendFloat("edgeGlow", 2.4);
venomShader.sendFloat("gloss", 0.92);
}
function addPart(mesh, x, y, z, sx, sy, sz, r, g, b) {
local m = eve.Renderable3D();
m.setMesh(mesh);
m.setShader(venomShader);
m.setPosition(x, y, z);
m.setScale(sx, sy, sz);
m.setTint(r, g, b, 1.0);
m.setRoughness(0.55);
m.setCastShadow(true);
m.setReceiveShadow(true);
venomParts.push(m);
return m;
}
function addTendril(seed) {
local t = {
body = eve.Renderable3D()
@@ -85,34 +73,46 @@ function addDroplet(seed) {
venomDroplets.push(d);
}
function buildMannequin() {
local skinR = 0.78, skinG = 0.58, skinB = 0.48;
local clothR = 0.18, clothG = 0.20, clothB = 0.28;
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";
addPart(gfx.newMeshSphere(28, 18), 0.0, 1.62, 0.0, 0.22, 0.26, 0.22, skinR, skinG, skinB);
addPart(gfx.newMeshCylinder(18, 1, true), 0.0, 1.38, 0.0, 0.10, 0.14, 0.10, skinR, skinG, skinB);
addPart(gfx.newMeshCylinder(20, 1, true), 0.0, 0.95, 0.0, 0.28, 0.55, 0.18, clothR, clothG, clothB);
addPart(gfx.newMeshSphere(16, 12), 0.0, 1.18, 0.0, 0.30, 0.16, 0.20, clothR, clothG, clothB);
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);
part.setYaw(3.14159); // face the orbit camera
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";
addPart(gfx.newMeshCylinder(12, 1, true), -0.38, 1.05, 0.0, 0.08, 0.42, 0.08, skinR, skinG, skinB);
addPart(gfx.newMeshCylinder(12, 1, true), 0.38, 1.05, 0.0, 0.08, 0.42, 0.08, skinR, skinG, skinB);
addPart(gfx.newMeshSphere(10, 8), -0.38, 0.82, 0.0, 0.09, 0.09, 0.09, skinR, skinG, skinB);
addPart(gfx.newMeshSphere(10, 8), 0.38, 0.82, 0.0, 0.09, 0.09, 0.09, skinR, skinG, skinB);
addPart(gfx.newMeshCylinder(12, 1, true), -0.12, 0.40, 0.0, 0.10, 0.55, 0.10,
clothR * 0.85, clothG * 0.85, clothB * 0.9);
addPart(gfx.newMeshCylinder(12, 1, true), 0.12, 0.40, 0.0, 0.10, 0.55, 0.10,
clothR * 0.85, clothG * 0.85, clothB * 0.9);
addPart(gfx.newMeshSphere(10, 8), -0.12, 0.08, 0.18, 0.11, 0.06, 0.18, 0.08, 0.08, 0.10);
addPart(gfx.newMeshSphere(10, 8), 0.12, 0.08, 0.18, 0.11, 0.06, 0.18, 0.08, 0.08, 0.10);
for (local i = 0; i < 8; ++i) addTendril(i);
for (local i = 0; i < 12; ++i) addDroplet(i);
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.05, 0.0);
venomGround.setScale(6.0, 0.08, 6.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);
@@ -120,45 +120,45 @@ function buildMannequin() {
}
function updateTendrils(_dt) {
local frontY = venomCoverage * 1.75;
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.785;
local radius = 0.22 + 0.18 * bloom + 0.05 * sin(venomTime * 2.1 + t.seed);
local y = frontY + 0.08 * sin(venomTime * 3.0 + t.seed) - 0.05;
local len = (0.15 + 0.55 * bloom) * (0.7 + 0.3 * sin(venomTime * 1.7 + i));
local thick = bloom > 0.05 ? (0.018 + 0.03 * bloom) : 0.001;
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.35 + 0.55 * sin(venomTime * 2.4 + t.seed);
t.body.setPosition(cos(ang) * radius, y, sin(ang) * radius * 0.65);
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 * 1.75;
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.15 + d.life * 0.55;
local y = frontY + 0.25 * d.life - 0.05;
local size = active ? (0.015 + 0.03 * (1.0 - d.life)) : 0.001;
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() {
local radius = 3.4;
local eyeY = 1.15;
local radius = 3.6;
local eyeY = 1.25;
venomCamera.setEye(sin(venomYaw) * radius, eyeY, cos(venomYaw) * radius);
venomCamera.setTarget(0.0, 0.95, 0.0);
}
@@ -180,10 +180,10 @@ eve_init = function() {
venomCamera = eve.Camera3D();
venomCamera.setUp(0.0, 1.0, 0.0);
venomCamera.setFov(40.0);
venomCamera.setAmbient(0.22, 0.18, 0.24);
venomCamera.setFov(38.0);
venomCamera.setAmbient(0.28, 0.24, 0.30);
venomCamera.setActive(true);
gfx.setDirectionalLight(-0.35, 0.85, 0.40, 1.55, 1.35, 1.2);
gfx.setDirectionalLight(-0.35, 0.90, 0.35, 1.65, 1.40, 1.25);
local rc = gfx.getRenderControl();
rc.disable("ao");
@@ -191,9 +191,10 @@ eve_init = function() {
rc.enable("msaa");
rc.compile();
if (venomParts.len() == 0) buildMannequin();
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");
};
@@ -201,7 +202,7 @@ eve_init = function() {
eve_update = function(dt) {
venomFrame += 1;
venomTime += dt;
venomYaw += dt * 0.25;
venomYaw += dt * 0.22;
orbitCamera();
if (keyJust("space")) venomPlaying = !venomPlaying;
@@ -228,14 +229,14 @@ eve_update = function(dt) {
local target = venomAutoShots[venomCaptureIdx];
local diff = target - venomCoverage;
if (fabs(diff) > 0.01) {
local step = (diff > 0.0 ? 1.0 : -1.0) * dt * 0.55;
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.35 && !venomCapturePending) {
if (venomAutoHold > 0.40 && !venomCapturePending) {
requestShot(format("captures/venom-%02d-c%.0f.png",
venomCaptureIdx, target * 100.0));
}
@@ -243,13 +244,13 @@ eve_update = function(dt) {
} else if (!venomDone && !venomCapturePending) {
venomDone = true;
print("[venom] auto demo complete\n");
// Prefer the load.nut bootBench early-exit so the window is not
// 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.22;
venomCoverage += dt * 0.20;
if (venomCoverage >= 1.0) {
venomCoverage = 1.0;
venomPlaying = false;
+18 -10
View File
@@ -1,5 +1,5 @@
#version 450
// Height + noise coverage: human tint -> glossy black goo with emissive edge.
// Height + noise coverage over textured mesh: albedo -> glossy black goo.
// Push constants (declareFloat order):
// 0 coverage 1 edgeWidth 2 noiseScale 3 time
// 4 gooR 5 gooG 6 gooB 7 edgeR 8 edgeG 9 edgeB 10 edgeGlow 11 gloss
@@ -34,6 +34,8 @@ layout(set = 0, binding = 0, std140) uniform Frame {
vec4 cloudWind;
} ubo;
layout(set = 0, binding = 1) uniform sampler2D albedoSampler;
layout(push_constant) uniform Externals {
float data[32];
} pc;
@@ -81,24 +83,30 @@ void main() {
if (dot(N, V) < 0.0)
N = -N;
// Bottom-to-top organic front.
float height01 = clamp((vWorldPos.y + 0.05) / 1.85, 0.0, 1.0);
// Quaternius FullBody is ~1.8 m tall with feet near y=0.
float height01 = clamp((vWorldPos.y + 0.02) / 1.80, 0.0, 1.0);
float n = fbm(vWorldPos.xz * noiseScale + vec2(time * 0.35, time * 0.22));
n = mix(n, fbm(vUV * noiseScale * 3.0 + time * 0.15), 0.35);
float field = height01 + (n - 0.5) * 0.28;
float frontier = coverage * 1.18 - 0.06;
// 0 = human, 1 = goo. Coverage grows upward from the feet.
float field = height01 + (n - 0.5) * 0.22;
float frontier = coverage * 1.15 - 0.04;
// 0 = human (textured), 1 = goo. Coverage grows upward from the feet.
float mask = 1.0 - smoothstep(frontier - edgeWidth, frontier + edgeWidth * 0.35, field);
float edge = (1.0 - smoothstep(0.0, edgeWidth * 2.2, abs(field - frontier)))
float edge = (1.0 - smoothstep(0.0, edgeWidth * 2.0, abs(field - frontier)))
* step(0.02, coverage) * step(coverage, 0.98);
vec4 albedoSample = texture(albedoSampler, vUV);
vec3 albedo = albedoSample.rgb * vTint.rgb;
// Keep a usable tint if the mesh has no albedo texture bound.
if (dot(albedoSample.rgb, albedoSample.rgb) < 1e-4)
albedo = vTint.rgb;
float ndotl = max(dot(N, L), 0.0);
vec3 H = normalize(L + V);
float spec = pow(max(dot(N, H), 0.0), mix(16.0, 96.0, gloss));
float fresnel = pow(1.0 - max(dot(N, V), 0.0), 3.0);
vec3 human = vTint.rgb * (0.22 + 0.78 * ndotl);
human += ubo.ambient.rgb * vTint.rgb * 0.55;
vec3 human = albedo * (0.25 + 0.75 * ndotl);
human += ubo.ambient.rgb * albedo * 0.55;
vec3 goo = gooColor * (0.08 + 0.55 * ndotl);
goo += ubo.lightColor.rgb * spec * gloss * 1.35;
@@ -110,5 +118,5 @@ void main() {
vec3 color = mix(human, goo, mask);
color += edgeColor * edge * edge * edgeGlow;
outColor = vec4(color, vTint.a);
outColor = vec4(color, albedoSample.a * vTint.a);
}