Files
EzVibe/assets/live2d/miku/viewer.html
e2hang 798e5c2f7d checkpoint: segfault root cause analysis + Live2D stability fixes
- _refresh_messages: widget pool reuse to avoid layout cascade → QWebEngineView crash
- viewer.html: QWebChannel bridge, texture resize, burst render, gl.finish
- live2d_view.py: debug checkpoints, playMotion/setExpression render-on-demand
- main.py: Chromium flags --no-sandbox --in-process-gpu --disable-gpu-rasterization --ignore-gpu-blocklist
- scheduler.py: test_reminder re-enabled
- docs: complete root cause analysis
2026-05-23 13:33:58 +08:00

291 lines
8.5 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>EzVibe Live2D Viewer</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;}
body{background:rgba(0,0,0,0);overflow:hidden;width:100%;height:100%;}
#canvas{width:100%;height:100%;display:block;}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="live2dcubismcore.min.js"></script>
<script>
(function(){
var model = null;
var canvas = document.getElementById('canvas');
var gl = null;
var ready = false;
window.resizeLive2D = function(w, h) {
canvas.width = w;
canvas.height = h;
if(gl) {
gl.viewport(0, 0, canvas.width, canvas.height);
}
};
window.playMotion = function(name) {
console.log('[Live2D] playMotion:', name);
};
window.setExpression = function(name) {
console.log('[Live2D] setExpression:', name);
};
window.setRandomMotion = function() {
console.log('[Live2D] setRandomMotion');
};
function notifyReady() {
if(ready && window.onLive2DReady) window.onLive2DReady();
}
// Compile shader
function compileShader(src, type) {
var shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('[Live2D] Shader compile error:', gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
// Create program
function createProgram(vsSrc, fsSrc) {
var vs = compileShader(vsSrc, gl.VERTEX_SHADER);
var fs = compileShader(fsSrc, gl.FRAGMENT_SHADER);
if(!vs || !fs) return null;
var prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
if(!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.error('[Live2D] Program link error:', gl.getProgramInfoLog(prog));
return null;
}
return prog;
}
var prog = null;
var a_position = -1, a_uv = -1;
var u_mvp = -1, u_tex = -1, u_alpha = -1;
function initGL() {
gl = canvas.getContext('webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if(!gl) {
console.error('[Live2D] WebGL not available');
return false;
}
// Simple 2D shader for Live2D rendering
prog = createProgram(
'attribute vec2 a_position; attribute vec2 a_uv; varying vec2 v_uv; uniform mat4 u_mvp; void main(){ v_uv = a_uv; gl_Position = u_mvp * vec4(a_position, 0.0, 1.0); }',
'precision mediump float; uniform sampler2D u_tex; uniform float u_alpha; varying vec2 v_uv; void main(){ gl_FragColor = texture2D(u_tex, v_uv) * u_alpha; }'
);
if(!prog) return false;
a_position = gl.getAttribLocation(prog, 'a_position');
a_uv = gl.getAttribLocation(prog, 'a_uv');
u_mvp = gl.getUniformLocation(prog, 'u_mvp');
u_tex = gl.getUniformLocation(prog, 'u_tex');
u_alpha = gl.getUniformLocation(prog, 'u_alpha');
// Initial projection matrix (200x200 canvas)
var proj = new Float32Array([
2/200, 0, 0, 0,
0, -2/200, 0, 0,
0, 0, 1, 0,
-1, 1, 0, 1
]);
gl.useProgram(prog);
gl.uniformMatrix4fv(u_mvp, false, proj);
gl.uniform1i(u_tex, 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
return true;
}
function loadTexture(img) {
return new Promise(function(resolve) {
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
resolve(tex);
});
}
var textures = [];
var drawables = [];
var meshes = [];
function updateMeshes() {
if(!model || !gl) return;
var count = model.drawables.count;
for(var i = 0; i < count; i++) {
var positions = model.drawables.vertexPositions[i];
var uvs = model.drawables.vertexUvs[i];
var indices = model.drawables.indices[i];
var texIdx = model.drawables.textureIndices[i];
var opacity = model.drawables.opacities[i];
if(!positions || !uvs || !indices) continue;
// Build interleaved buffer: position(2) + uv(2)
var vertCount = positions.length;
var vertexData = new Float32Array(vertCount * 4);
for(var j = 0; j < vertCount; j++) {
vertexData[j * 4 + 0] = positions[j][0];
vertexData[j * 4 + 1] = positions[j][1];
vertexData[j * 4 + 2] = uvs[j][0];
vertexData[j * 4 + 3] = uvs[j][1];
}
var vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
var ibo = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
meshes[i] = {
vbo: vbo,
ibo: ibo,
count: indices.length,
texture: textures[texIdx] || null,
opacity: opacity || 1.0
};
}
}
function render() {
if(!gl || !model) {
requestAnimationFrame(render);
return;
}
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(prog);
gl.enableVertexAttribArray(a_position);
gl.enableVertexAttribArray(a_uv);
for(var i = 0; i < meshes.length; i++) {
var m = meshes[i];
if(!m || !m.texture) continue;
gl.uniform1f(u_alpha, m.opacity);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, m.texture);
gl.bindBuffer(gl.ARRAY_BUFFER, m.vbo);
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 16, 0);
gl.vertexAttribPointer(a_uv, 2, gl.FLOAT, false, 16, 8);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, m.ibo);
gl.drawElements(gl.TRIANGLES, m.count, gl.UNSIGNED_SHORT, 0);
}
requestAnimationFrame(render);
}
async function init() {
try {
if(!initGL()) return;
canvas.width = 200;
canvas.height = 200;
// Load model JSON
var modelJsonResp = await fetch('miku.model.json');
var modelJson = await modelJsonResp.json();
// Load moc file
var mocResp = await fetch(modelJson.model);
var mocBuf = await mocResp.arrayBuffer();
// Try Cubism 4.x Core API first
var moc = Live2DCubismCore.Moc.fromArrayBuffer(mocBuf);
if(moc) {
console.log('[Live2D] Cubism 4.x moc detected');
model = Live2DCubismCore.Model.fromMoc(moc);
if(model) {
console.log('[Live2D] Cubism 4.x model created, drawables:', model.drawables.count);
}
}
// If Cubism 4.x failed, try Cubism 2.x
if(!model || !model.drawables) {
console.log('[Live2D] Trying Cubism 2.x format...');
// Cubism 2.x: check if Live2D global exists
if(typeof Live2D !== 'undefined') {
Live2D.init();
model = Live2D.WebGLModel.createFromBuffer(mocBuf);
console.log('[Live2D] Cubism 2.x model created');
}
}
if(!model || !model.drawables) {
console.error('[Live2D] Failed to create model from either version');
document.body.innerHTML = '<div style="color:red;padding:20px;">Live2D model creation failed</div>';
return;
}
// Load textures
for(var i = 0; i < modelJson.textures.length; i++) {
var img = new Image();
img.crossOrigin = 'anonymous';
await new Promise(function(resolve, reject) {
img.onload = function() {
loadTexture(img).then(function(tex) {
textures[i] = tex;
resolve();
});
};
img.onerror = function() {
console.error('[Live2D] Failed to load texture:', modelJson.textures[i]);
textures[i] = null;
resolve();
};
img.src = modelJson.textures[i];
});
}
updateMeshes();
// Update model periodically
setInterval(function() {
if(model && model.update) {
model.update();
}
}, 16);
ready = true;
console.log('[Live2D] Miku loaded successfully!');
notifyReady();
render();
} catch(e) {
console.error('[Live2D] Init error:', e);
document.body.innerHTML = '<div style="color:red;padding:20px;">Live2D error: ' + e.message + '</div>';
}
}
window.addEventListener('load', init);
})();
</script>
</body>
</html>