- _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
563 lines
20 KiB
HTML
563 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<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 src="qwebchannel.js"></script>
|
|
<script>
|
|
(function(){
|
|
var canvas = document.getElementById('canvas');
|
|
var gl = null;
|
|
var model = null;
|
|
var program = null;
|
|
var ready = false;
|
|
var _assetsDir = '';
|
|
var _alreadyInitialized = false;
|
|
var _renderRequested = false;
|
|
|
|
// Max texture dimension to avoid GPU memory exhaustion (4096^2*4*2 = 128MB)
|
|
var MAX_TEXTURE_SIZE = 1024;
|
|
|
|
function _glError(tag) {
|
|
var err = gl.getError();
|
|
if(err !== gl.NO_ERROR) {
|
|
console.error('[Live2D] GL error after ' + tag + ': 0x' + err.toString(16));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Simple WebGL shader for Live2D rendering
|
|
var VERTEX_SHADER = [
|
|
'precision mediump float;',
|
|
'attribute vec2 a_position;',
|
|
'attribute vec2 a_uv;',
|
|
'varying vec2 v_uv;',
|
|
'uniform mat4 u_mvp;',
|
|
'void main() {',
|
|
' gl_Position = u_mvp * vec4(a_position, 0.0, 1.0);',
|
|
' v_uv = a_uv;',
|
|
'}'
|
|
].join('\n');
|
|
|
|
var FRAGMENT_SHADER = [
|
|
'precision mediump float;',
|
|
'varying vec2 v_uv;',
|
|
'uniform sampler2D u_texture;',
|
|
'uniform vec4 u_color;',
|
|
'void main() {',
|
|
' vec4 c = texture2D(u_texture, v_uv);',
|
|
' gl_FragColor = c * u_color;',
|
|
'}'
|
|
].join('\n');
|
|
|
|
function createShader(type, source) {
|
|
var shader = gl.createShader(type);
|
|
gl.shaderSource(shader, source);
|
|
gl.compileShader(shader);
|
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
console.error('[Live2D] Shader compile error:', gl.getShaderInfoLog(shader));
|
|
gl.deleteShader(shader);
|
|
return null;
|
|
}
|
|
return shader;
|
|
}
|
|
|
|
function createProgram(vs, fs) {
|
|
var p = gl.createProgram();
|
|
gl.attachShader(p, vs);
|
|
gl.attachShader(p, fs);
|
|
gl.linkProgram(p);
|
|
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
|
|
console.error('[Live2D] Program link error:', gl.getProgramInfoLog(p));
|
|
return null;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
function base64ToArrayBuffer(base64) {
|
|
var binary = atob(base64);
|
|
var len = binary.length;
|
|
var bytes = new Uint8Array(len);
|
|
for (var i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
|
|
return bytes.buffer;
|
|
}
|
|
|
|
// Draw a test rectangle to verify WebGL pipeline works
|
|
function _renderTestRect() {
|
|
console.log('[Live2D] Rendering test rectangle...');
|
|
var testVerts = new Float32Array([
|
|
-0.5, 0.5, 0, 0,
|
|
0.5, 0.5, 1, 0,
|
|
-0.5, -0.5, 0, 1,
|
|
0.5, -0.5, 1, 1
|
|
]);
|
|
var testIdx = new Uint16Array([0, 1, 2, 1, 3, 2]);
|
|
|
|
var vbuf = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, vbuf);
|
|
gl.bufferData(gl.ARRAY_BUFFER, testVerts, gl.STATIC_DRAW);
|
|
|
|
var ibuf = gl.createBuffer();
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf);
|
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, testIdx, gl.STATIC_DRAW);
|
|
|
|
gl.useProgram(program);
|
|
var posLoc = gl.getAttribLocation(program, 'a_position');
|
|
var uvLoc = gl.getAttribLocation(program, 'a_uv');
|
|
gl.enableVertexAttribArray(posLoc);
|
|
gl.enableVertexAttribArray(uvLoc);
|
|
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 16, 0);
|
|
gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 16, 8);
|
|
|
|
var mvp = new Float32Array([2,0,0,0, 0,-2,0,0, 0,0,1,0, 0,0,0,1]);
|
|
gl.uniformMatrix4fv(gl.getUniformLocation(program, 'u_mvp'), false, mvp);
|
|
|
|
// 1x1 white pixel texture
|
|
var whiteTex = gl.createTexture();
|
|
gl.bindTexture(gl.TEXTURE_2D, whiteTex);
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
|
|
new Uint8Array([255,255,255,255]));
|
|
gl.uniform1i(gl.getUniformLocation(program, 'u_texture'), 0);
|
|
gl.uniform4f(gl.getUniformLocation(program, 'u_color'), 1, 1, 1, 1);
|
|
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf);
|
|
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
|
|
|
|
gl.deleteBuffer(vbuf);
|
|
gl.deleteBuffer(ibuf);
|
|
gl.deleteTexture(whiteTex);
|
|
console.log('[Live2D] Test rectangle rendered OK');
|
|
}
|
|
|
|
// Resize texture image to avoid GPU memory exhaustion
|
|
function _resizeTexture(img, maxSize, callback) {
|
|
if(img.width <= maxSize && img.height <= maxSize) {
|
|
callback(null, img);
|
|
return;
|
|
}
|
|
var scale = Math.min(maxSize / img.width, maxSize / img.height);
|
|
var w = Math.floor(img.width * scale);
|
|
var h = Math.floor(img.height * scale);
|
|
console.log('[Live2D] Resizing texture from ' + img.width + 'x' + img.height + ' to ' + w + 'x' + h);
|
|
var c = document.createElement('canvas');
|
|
c.width = w;
|
|
c.height = h;
|
|
var ctx = c.getContext('2d');
|
|
ctx.drawImage(img, 0, 0, w, h);
|
|
callback(null, c);
|
|
}
|
|
|
|
window.resizeLive2D = function(w, h) {
|
|
if(gl) {
|
|
canvas.width = w;
|
|
canvas.height = h;
|
|
gl.viewport(0, 0, w, h);
|
|
}
|
|
};
|
|
|
|
window.playMotion = function(name) {
|
|
console.log('[Live2D] playMotion:', name);
|
|
if(window.requestRender) window.requestRender();
|
|
};
|
|
|
|
window.setExpression = function(name) {
|
|
console.log('[Live2D] setExpression:', name);
|
|
if(window.requestRender) window.requestRender();
|
|
};
|
|
|
|
window.setRandomMotion = function() {
|
|
console.log('[Live2D] setRandomMotion');
|
|
if(window.requestRender) window.requestRender();
|
|
};
|
|
|
|
window._initLive2DAsync = async function(modelJson, mocBase64, mocFilename, assetsDir) {
|
|
if(_alreadyInitialized) {
|
|
console.log('[Live2D] Already initialized, skipping');
|
|
return;
|
|
}
|
|
_alreadyInitialized = true;
|
|
console.log('[Live2D] _initLive2DAsync called');
|
|
if(window.pybridge) window.pybridge.onJsMessage('init_start');
|
|
|
|
_assetsDir = assetsDir || '';
|
|
|
|
if(typeof Live2DCubismCore === 'undefined') {
|
|
console.error('[Live2D] Cubism Core not loaded');
|
|
if(window.pybridge) window.pybridge.onJsMessage('ERR:cubism_not_loaded');
|
|
return;
|
|
}
|
|
|
|
// Init WebGL
|
|
canvas.width = 200;
|
|
canvas.height = 200;
|
|
gl = canvas.getContext('webgl', {alpha: true, premultipliedAlpha: true})
|
|
|| canvas.getContext('experimental-webgl', {alpha: true, premultipliedAlpha: true});
|
|
if(!gl) {
|
|
console.error('[Live2D] WebGL not available');
|
|
if(window.pybridge) window.pybridge.onJsMessage('ERR:no_webgl');
|
|
return;
|
|
}
|
|
console.log('[Live2D] WebGL initialized, renderer:', gl.getParameter(gl.RENDERER));
|
|
if(window.pybridge) window.pybridge.onJsMessage('webgl_ok:' + gl.getParameter(gl.RENDERER));
|
|
|
|
gl.viewport(0, 0, 200, 200);
|
|
gl.clearColor(0, 0, 0, 0);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
gl.enable(gl.BLEND);
|
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
_glError('init');
|
|
|
|
// Create shader program
|
|
var vs = createShader(gl.VERTEX_SHADER, VERTEX_SHADER);
|
|
var fs = createShader(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
|
|
if(!vs || !fs) {
|
|
console.error('[Live2D] Shader creation failed');
|
|
if(window.pybridge) window.pybridge.onJsMessage('ERR:shader_compile');
|
|
return;
|
|
}
|
|
program = createProgram(vs, fs);
|
|
if(!program) {
|
|
console.error('[Live2D] Program creation failed');
|
|
if(window.pybridge) window.pybridge.onJsMessage('ERR:program_link');
|
|
return;
|
|
}
|
|
console.log('[Live2D] Shader program created');
|
|
if(window.pybridge) window.pybridge.onJsMessage('shader_ok');
|
|
|
|
// Verify WebGL works with a test rectangle
|
|
try {
|
|
_renderTestRect();
|
|
_glError('testRect');
|
|
if(window.pybridge) window.pybridge.onJsMessage('test_rect_ok');
|
|
} catch(e) {
|
|
console.error('[Live2D] Test rect failed:', e.message);
|
|
if(window.pybridge) window.pybridge.onJsMessage('ERR:test_rect:' + e.message);
|
|
return;
|
|
}
|
|
|
|
// Decode moc
|
|
console.log('[Live2D] Decoding moc...');
|
|
if(window.pybridge) window.pybridge.onJsMessage('decoding_moc');
|
|
var mocBuf = base64ToArrayBuffer(mocBase64);
|
|
console.log('[Live2D] moc buffer size:', mocBuf.byteLength);
|
|
|
|
var moc = Live2DCubismCore.Moc.fromArrayBuffer(mocBuf);
|
|
if(!moc) {
|
|
console.error('[Live2D] Moc load failed');
|
|
if(window.pybridge) window.pybridge.onJsMessage('ERR:moc_load');
|
|
return;
|
|
}
|
|
|
|
model = Live2DCubismCore.Model.fromMoc(moc);
|
|
if(!model) {
|
|
console.error('[Live2D] Model build failed');
|
|
if(window.pybridge) window.pybridge.onJsMessage('ERR:model_build');
|
|
return;
|
|
}
|
|
window._live2d_model = model;
|
|
window._live2d_drawables = model.drawables;
|
|
console.log('[Live2D] Model created, drawables:', model.drawables.count);
|
|
if(window.pybridge) window.pybridge.onJsMessage('model_ok:d=' + model.drawables.count);
|
|
|
|
// Load textures (resized to avoid GPU memory exhaustion)
|
|
var textureUrls = modelJson.FileReferences.Textures;
|
|
console.log('[Live2D] Loading', textureUrls.length, 'textures...');
|
|
if(window.pybridge) window.pybridge.onJsMessage('loading_textures:n=' + textureUrls.length);
|
|
|
|
window._loadedTextures = [];
|
|
for(var i = 0; i < textureUrls.length; i++) {
|
|
var url = (_assetsDir ? _assetsDir + '/' : '') + textureUrls[i];
|
|
console.log('[Live2D] Loading texture ' + i + ': ' + url);
|
|
try {
|
|
var rawImg = await new Promise(function(resolve, reject) {
|
|
var img = new Image();
|
|
img.crossOrigin = 'anonymous';
|
|
img.onload = function() { resolve(img); };
|
|
img.onerror = function(e) { reject(e); };
|
|
img.src = url;
|
|
});
|
|
console.log('[Live2D] Texture ' + i + ' raw size:', rawImg.width, 'x', rawImg.height);
|
|
|
|
// Resize to avoid GPU memory pressure (4096x4096 = 64MB per texture)
|
|
var texSource = await new Promise(function(resolve, reject) {
|
|
_resizeTexture(rawImg, MAX_TEXTURE_SIZE, function(err, src) {
|
|
if(err) reject(err); else resolve(src);
|
|
});
|
|
});
|
|
rawImg = null; // release full-size 64MB bitmap for GC
|
|
|
|
var tex = gl.createTexture();
|
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
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);
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texSource);
|
|
if(_glError('texture' + i)) {
|
|
console.error('[Live2D] GL error uploading texture ' + i);
|
|
}
|
|
window._loadedTextures.push(tex);
|
|
console.log('[Live2D] Texture', i, 'uploaded OK');
|
|
if(window.pybridge) window.pybridge.onJsMessage('texture_ok:' + i);
|
|
// Small delay between textures to let GPU breathe
|
|
await new Promise(function(r) { setTimeout(r, 50); });
|
|
} catch(e) {
|
|
console.error('[Live2D] Texture load error:', e);
|
|
if(window.pybridge) window.pybridge.onJsMessage('ERR:texture:' + i + ':' + e);
|
|
}
|
|
}
|
|
|
|
console.log('[Live2D] Textures loaded: ' + window._loadedTextures.length);
|
|
if(window.pybridge) window.pybridge.onJsMessage('textures_done:n=' + window._loadedTextures.length);
|
|
|
|
// Pre-build cached buffers
|
|
if(window.pybridge) window.pybridge.onJsMessage('building_buffers');
|
|
try {
|
|
_buildCachedBuffers();
|
|
console.log('[Live2D] Cached buffers built: ' + _cachedBuffers.length);
|
|
if(window.pybridge) window.pybridge.onJsMessage('buffers_ok:n=' + _cachedBuffers.length);
|
|
} catch(e) {
|
|
console.error('[Live2D] Buffer build error:', e.message);
|
|
if(window.pybridge) window.pybridge.onJsMessage('ERR:buffers:' + e.message);
|
|
return;
|
|
}
|
|
|
|
ready = true;
|
|
document.title = 'LIVE2D_OK:d=' + model.drawables.count;
|
|
console.log('[Live2D] Ready, scheduling first render...');
|
|
if(window.pybridge) window.pybridge.onJsMessage('scheduling_render');
|
|
|
|
// Render first frame via setTimeout to avoid GPU calls during init
|
|
setTimeout(function() {
|
|
if(ready && gl && model) {
|
|
try {
|
|
renderFrame();
|
|
console.log('[Live2D] First frame rendered OK');
|
|
// Flush GPU command buffer and finish all pending operations.
|
|
// If the crash is from an async GPU operation (compositing/present),
|
|
// this will move the crash into this callback, before first_frame_ok is sent.
|
|
gl.flush();
|
|
gl.finish();
|
|
console.log('[Live2D] GPU flush+finish complete');
|
|
if(window.pybridge) window.pybridge.onJsMessage('first_frame_ok');
|
|
} catch(e) {
|
|
console.error('[Live2D] First frame error:', e.message);
|
|
if(window.pybridge) window.pybridge.onJsMessage('ERR:first_frame:' + e.message);
|
|
}
|
|
}
|
|
}, 50);
|
|
|
|
// Notify Python side
|
|
if(window.pybridge) {
|
|
window.pybridge.onJsMessage('ready');
|
|
}
|
|
};
|
|
|
|
var _cachedBuffers = [];
|
|
|
|
function _buildCachedBuffers() {
|
|
console.log('[Live2D] Building cached buffers for ' + model.drawables.count + ' drawables...');
|
|
var drawables = model.drawables;
|
|
_cachedBuffers = [];
|
|
var successCount = 0;
|
|
for(var i = 0; i < drawables.count; i++) {
|
|
try {
|
|
var vc = drawables.vertexCounts[i];
|
|
var ic = drawables.indexCounts[i];
|
|
if(vc <= 0 || ic <= 0) {
|
|
_cachedBuffers.push(null);
|
|
continue;
|
|
}
|
|
var vertexOffset = 0;
|
|
var indexOffset = 0;
|
|
for(var j = 0; j < i; j++) {
|
|
vertexOffset += drawables.vertexCounts[j];
|
|
indexOffset += drawables.indexCounts[j];
|
|
}
|
|
var vdata = new Float32Array(vc * 4);
|
|
var idata = new Uint16Array(ic);
|
|
var posArr = drawables.vertexPositions;
|
|
var uvArr = drawables.vertexUvs;
|
|
var idxArr = drawables.indices;
|
|
for(var v = 0; v < vc; v++) {
|
|
var px = posArr[(vertexOffset + v) * 2];
|
|
var py = posArr[(vertexOffset + v) * 2 + 1];
|
|
if(isNaN(px) || isNaN(py)) { px = 0; py = 0; }
|
|
vdata[v * 4] = px;
|
|
vdata[v * 4 + 1] = py;
|
|
vdata[v * 4 + 2] = uvArr[(vertexOffset + v) * 2];
|
|
vdata[v * 4 + 3] = uvArr[(vertexOffset + v) * 2 + 1];
|
|
}
|
|
for(var ii = 0; ii < ic; ii++) {
|
|
idata[ii] = idxArr[indexOffset + ii];
|
|
}
|
|
var vbuf = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, vbuf);
|
|
gl.bufferData(gl.ARRAY_BUFFER, vdata, gl.DYNAMIC_DRAW);
|
|
var ibuf = gl.createBuffer();
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf);
|
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, idata, gl.STATIC_DRAW);
|
|
_cachedBuffers.push({vbuf: vbuf, ibuf: ibuf, vc: vc, ic: ic});
|
|
successCount++;
|
|
} catch(e) {
|
|
console.error('[Live2D] Buffer build error for drawable ' + i + ':', e.message);
|
|
_cachedBuffers.push(null);
|
|
}
|
|
}
|
|
console.log('[Live2D] Built ' + successCount + '/' + drawables.count + ' buffers');
|
|
_glError('buildBuffers');
|
|
}
|
|
|
|
function renderFrame() {
|
|
if(!ready || !gl || !model) return;
|
|
|
|
try {
|
|
var w = canvas.width || 200;
|
|
var h = canvas.height || 200;
|
|
|
|
gl.viewport(0, 0, w, h);
|
|
gl.clearColor(0, 0, 0, 0);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
|
|
var drawables = model.drawables;
|
|
if(!drawables || !drawables.count || drawables.count <= 0) return;
|
|
if(!drawables.vertexPositions || !drawables.vertexUvs || !drawables.indices) return;
|
|
|
|
if(_cachedBuffers.length !== drawables.count) {
|
|
_buildCachedBuffers();
|
|
}
|
|
|
|
var count = drawables.count;
|
|
var sortedIndices = [];
|
|
for(var i = 0; i < count; i++) sortedIndices.push(i);
|
|
sortedIndices.sort(function(a, b) {
|
|
return drawables.drawOrders[a] - drawables.drawOrders[b];
|
|
});
|
|
|
|
var posArr = drawables.vertexPositions;
|
|
var uvArr = drawables.vertexUvs;
|
|
|
|
// Cache uniform/program locations once per frame
|
|
gl.useProgram(program);
|
|
var posLoc = gl.getAttribLocation(program, 'a_position');
|
|
var uvLoc = gl.getAttribLocation(program, 'a_uv');
|
|
var mvpLoc = gl.getUniformLocation(program, 'u_mvp');
|
|
var colorLoc = gl.getUniformLocation(program, 'u_color');
|
|
var texLoc = gl.getUniformLocation(program, 'u_texture');
|
|
|
|
var sx = 2.0 / w;
|
|
var sy = 2.0 / h;
|
|
var mvp = new Float32Array([sx, 0, 0, 0, 0, -sy, 0, 0, 0, 0, 1, 0, -1, 1, 0, 1]);
|
|
gl.uniformMatrix4fv(mvpLoc, false, mvp);
|
|
gl.uniform1i(texLoc, 0);
|
|
|
|
var renderedCount = 0;
|
|
for(var si = 0; si < sortedIndices.length; si++) {
|
|
var i = sortedIndices[si];
|
|
var vc = drawables.vertexCounts[i];
|
|
var ic = drawables.indexCounts[i];
|
|
var texIdx = drawables.textureIndices[i];
|
|
var opacity = drawables.opacities[i];
|
|
|
|
if(vc <= 0 || ic <= 0) continue;
|
|
if(texIdx < 0 || texIdx >= window._loadedTextures.length) continue;
|
|
if(opacity <= 0) continue;
|
|
|
|
var cached = _cachedBuffers[i];
|
|
if(!cached) continue;
|
|
|
|
var tex = window._loadedTextures[texIdx];
|
|
if(!tex) continue;
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, cached.vbuf);
|
|
gl.enableVertexAttribArray(posLoc);
|
|
gl.enableVertexAttribArray(uvLoc);
|
|
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 16, 0);
|
|
gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 16, 8);
|
|
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cached.ibuf);
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
gl.uniform4f(colorLoc, 1, 1, 1, opacity);
|
|
|
|
gl.drawElements(gl.TRIANGLES, ic, gl.UNSIGNED_SHORT, 0);
|
|
renderedCount++;
|
|
}
|
|
if(renderedCount === 0) {
|
|
console.warn('[Live2D] Nothing rendered (check drawables, textures, opacities)');
|
|
}
|
|
} catch(e) {
|
|
console.error('[Live2D] Render error:', e.message, e.stack);
|
|
}
|
|
}
|
|
|
|
// Render on demand (no continuous timer)
|
|
window.requestRender = function() {
|
|
if(!ready || _renderRequested) return;
|
|
_renderRequested = true;
|
|
setTimeout(function() {
|
|
_renderRequested = false;
|
|
if(ready && gl && model) {
|
|
try { renderFrame(); } catch(e) { console.error('[Live2D] requestRender error:', e.message); }
|
|
}
|
|
}, 16);
|
|
};
|
|
|
|
// Auto-init: wait for model data then call init
|
|
function tryAutoInit() {
|
|
if(typeof Live2DCubismCore === 'undefined') {
|
|
setTimeout(tryAutoInit, 100);
|
|
return;
|
|
}
|
|
if(!window._modelJson || !window._mocBase64) {
|
|
setTimeout(tryAutoInit, 100);
|
|
return;
|
|
}
|
|
console.log('[Live2D] Auto-init: calling _initLive2DAsync');
|
|
window._initLive2DAsync(window._modelJson, window._mocBase64, window._mocFileName || '', window._assetsDir || '');
|
|
}
|
|
|
|
// Initialize QWebChannel bridge (qt.webChannelTransport auto-injected by Qt)
|
|
function _setupChannel() {
|
|
if(typeof QWebChannel === 'undefined') {
|
|
// qwebchannel.js not loaded yet; retry
|
|
setTimeout(_setupChannel, 50);
|
|
return;
|
|
}
|
|
if(!window.qt || !window.qt.webChannelTransport) {
|
|
setTimeout(_setupChannel, 50);
|
|
return;
|
|
}
|
|
try {
|
|
new QWebChannel(qt.webChannelTransport, function(channel) {
|
|
window.pybridge = channel.objects.pybridge;
|
|
console.log('[Live2D] QWebChannel ready, pybridge=' + typeof window.pybridge);
|
|
// Now safe to auto-init
|
|
tryAutoInit();
|
|
});
|
|
} catch(e) {
|
|
console.error('[Live2D] QWebChannel setup error:', e);
|
|
}
|
|
}
|
|
_setupChannel();
|
|
|
|
// Expose for Python (called from runJavaScript injection)
|
|
window.startLive2D = function() {
|
|
if(window._modelJson && window._mocBase64) {
|
|
window._initLive2DAsync(window._modelJson, window._mocBase64, window._mocFileName || '', window._assetsDir || '');
|
|
}
|
|
};
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|