- _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
291 lines
8.5 KiB
HTML
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> |