\
\
\
\Chicken Rig Editor\
\
\\
\\
\\
\\
\\
\\
\
body {margin:0; переполнение: скрыто; фон:#222; семейство шрифтов: без засечек;
#ui {
position:absolute;
bottom:0;
width: 100%;
background: rgba(0,0,0,0.85);
padding: 6 пикселей;
display: flex;
flex-wrap:wrap;
кнопка, ввод { font-size:16px; поле: 4 пикселя;
#timeline { display:flex; переполнение-х: авто; ширина: 100%;
.frame {
ширина: 36 пикселей; высота: 36 пикселей; фон:#444; маржа: 4 пикселя;
радиус границы: 4 пикселя; выравнивание текста: по центру; высота строки: 36 пикселей; цвет:белый;
метка { цвет:белый;
\
\
\
\ \Повернуть\ \Переместить\ \Добавить кадр\ \Следующий кадр\ \Отменить\ \Повторить\ \Сброс установки\ \ Δt \\ \Play\ \Экспорт GLTF\
\\
\
\
let Scene, Camera, Render;
let Controls, TransformControls;
let Chicken;
let Bones = \[\];
let BoneDots = \[\];
let Frames = \[\];
let FrameTimes = \[\];
let undoStack = \[\];
let redoStack = \[\];
let restPose = \[\];
let play = false;
let playIndex = 0;
let playTimer = 0;
let Last = Performance.now();
const raycaster = new THREE.Raycaster();
raycaster.params.Sprite.threshold = 1.2;
const pointer = new THREE.Vector2();
let isTouching = false;
let LastTouchY = 0;
/\* ================= INIT =============== \*/
init();
loadChicken();
animate();
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x444444);
camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 1000);
camera.position.set(0,3,8);
renderer = new THREE.WebGLRenderer({ antialias:true });
renderer.setSize(innerWidth, InnerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enablePan = true;
transformControls = new THREE.TransformControls(camera, renderer.domElement);
transformControls.addEventListener("dragging-changed", e =\> {
Код: Выделить всё
controls.enabled = !e.value;
scene.add(transformControls);
scene.add(new THREE.AmbientLight(0xffffff,0.7));
const d = new THREE.DirectionalLight(0xffffff,0.8);
d.position.set(5,10,5);
scene.add(d);
renderer.domElement.addEventListener("pointerdown", onPointerDown, { пассивный:false });
renderer.domElement.addEventListener("pointermove", onPointerMove, { пассивный:false) });
renderer.domElement.addEventListener("pointerup", ()=\>isTouching=false);
window.addEventListener("resize", ()=\>{
Код: Выделить всё
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
/\* ================ LOAD ================ \*/
function loadChicken() {
const loader = new THREE.FBXLoader();
const tex = new THREE.TextureLoader();
loader.load("https://itswardengod.github.io/wardengame/Chicken.FBX", obj=\>{
Код: Выделить всё
chicken = obj;
chicken.scale.setScalar(0.03);
const main = tex.load("https://itswardengod.github.io/wardengame/Main.png");
const alpha = tex.load("https://itswardengod.github.io/wardengame/Opacity.png");
obj.traverse(c=\\\>{
if(c.isMesh){
c.material.map = main;
c.material.alphaMap = alpha;
c.material.alphaTest = 0.5;
c.material.side = THREE.DoubleSide;
}
if(c.isBone){
bones.push(c);
restPose.push(c.quaternion.clone());
createBoneDot(c);
}
});
scene.add(chicken);
/\* ================ BONE DOT ================= \*/
function createBoneDot(bone){
const mat = new THREE.SpriteMaterial({ color:0xff5555, deepTest:false });
const s = new THREE.Sprite(mat);
s.scale.set(1.6,1.6,1.6); // БОЛЬШЕ ДЛЯ СЕНСА
s.renderOrder = 999;
bone.add(s);
boneDots.push({bone, sprite:s });
/\* ================ УКАЗАТЕЛЬ ================= \*/
function onPointerDown(e){
isTouching = true;
lastTouchY = e.clientY;
pointer.x = (e.clientX/innerWidth) \* 2 - 1;
pointer.y = -(e.clientY/innerHeight) \* 2 + 1;
raycaster.setFromCamera(pointer, camera);
const hits = raycaster.intersectObjects(boneDots.map(b=\>b.sprite), true);
if(hits.length){
Код: Выделить всё
const hit = boneDots.find(b=\\\>b.sprite===hits\\\[0\\\].object);
transformControls.attach(hit.bone);
e.preventDefault();
Код: Выделить всё
transformControls.detach();
}
function onPointerMove(e){
if(!isTouching) return;
// ПЕРЕТАЧИВАНИЕ ОДНЫМ ПАЛЬЦЕМ = ЗУМ
if(e.pointerType === "touch" && e.buttons === 1 && !transformControls.draging){
Код: Выделить всё
const dy = e.clientY - lastTouchY;
camera.position.addScaledVector(
camera.getWorldDirection(new THREE.Vector3()).negate(),
dy \\\* 0.01
);
lastTouchY = e.clientY;
e.preventDefault();
/\* ================ FRAMES ================ \*/
function snapshot(){ returnbones.map(b=\>b.quaternion.clone());
function saveFrame(){
undoStack.push({frames:\[...frames\], times:\[...frameTimes\]});
redoStack.length = 0;
frames.push(snapshot());
frameTimes.push(parseFloat(frameTime.value)||0.4);
rebuildTimeline();
функция nextFrame(){
if(!frames.length) return;
frames.at(-1).forEach((q,i)=\>bones\[i\].quaternion.copy(q));
function rebuildTimeline(){
timeline.innerHTML="";
frames.forEach((\_,i)=\>{
Код: Выделить всё
const d=document.createElement("div");
d.className="frame";
d.textContent=i;
d.onclick=()=\\\>frames\\\[i\\\].forEach((q,b)=\\\>bones\\\[b\\\].quaternion.copy(q));
timeline.appendChild(d);
/\* ================ UNDO ================ \*/
функция undo(){
if(!undoStack.length) return;
redoStack.push({frames:\[...frames\], times:\[...frameTimes\]});
({frames,frameTimes} = undoStack.pop());
rebuildTimeline();
function redo(){
if(!redoStack.length) return;
undoStack.push({frames:\[...frames\], times:\[...frameTimes\]});
({frames,frameTimes} = redoStack.pop());
rebuildTimeline();
/\* ================ RESET ================ \*/
function resetRig(){bones.forEach((b,i)=\>b.quaternion.copy(restPose\[i\]));
/\* ================= ANIMATION ================ \*/
function playAnimation(){
if(frames.length\=d){
Код: Выделить всё
playTimer=0; playIndex++;
if(playIndex\\\>=frames.length-1){ playing=false; return; }
const a=playTimer/d;
bones.forEach((b,i)=\>b.quaternion.slerpQuaternions(
Код: Выделить всё
frames\\\[playIndex\\\]\\\[i\\\],
frames\\\[playIndex+1\\\]\\\[i\\\], a));
/\* ================ EXPORT ================= \*/
function ExportGLTF(){
const track=\[\]; пусть t=0; const times=\[0\];
frameTimes.forEach(d=\>{t+=d;times.push(t);});
bones.forEach((bone,i)=\>{
Код: Выделить всё
const v=\\\[\\\];
frames.forEach(f=\\\>{const q=f\\\[i\\\]; v.push(q.x,q.y,q.z,q.w);});
tracks.push(new THREE.QuaternionKeyframeTrack(
bone.name+".quaternion", times, v));
const clip=new THREE.AnimationClip("RigAnimation",-1,tracks);
chicken.animations=\[clip\];
new THREE.GLTFExporter().parse(chicken,g=\>{
Код: Выделить всё
const a=document.createElement("a");
a.href=URL.createObjectURL(new Blob(\\\[JSON.stringify(g)\\\],{type:"application/json"}));
a.download="rigged_animation.gltf";
a.click();
/\* ================ LOOP =============== \*/
function animate(){
requestAnimationFrame(animate);
const now= Performance.now(), dt=(now-last)/1000; Last=now;
updateAnimation(dt);
controls.update();
renderer.render(сцена, камера);
}
function setMode(m){ TransformControls.setMode(m);
\
\
\ и вот вариант, который не так хорош, но более фиксирован \
\
\
\
\Chicken Rig Editor\
\
\\
\\
\\
\\
\\
\\
\
body {margin:0; переполнение: скрыто; фон:#222; семейство шрифтов: без засечек;
#ui {
position:absolute;
bottom:0;
width: 100%;
background: rgba(0,0,0,0.85);
padding: 6 пикселей;
display: flex;
flex-wrap:wrap;
кнопка, ввод { font-size:16px; поле: 4 пикселя;
#timeline { display:flex; переполнение-х: авто; ширина: 100%;
.frame {
ширина: 36 пикселей; высота: 36 пикселей; фон:#444; маржа: 4 пикселя;
радиус границы: 4 пикселя; выравнивание текста: по центру; высота строки: 36 пикселей; цвет:белый;
метка { цвет:белый;
\
\
\
\ \Повернуть\ \Переместить\ \Добавить кадр\ \Следующий кадр\ \Отменить\ \Повторить\ \Сброс установки\ \ Δt \ \ \Play\ \Экспорт GLTF\
\\
\
\
/\* ================= GLOBALS =============== \*/
let сцена, камера, рендерер;
let элементы управления, TransformControls;
let Chicken;
letbones = \[\];
letboneDots = \[\];
let Frames = \[\];
let FrameTimes = \[\];
let undoStack = \[\];
let redoStack = \[\];
let restPose = \[\];
let play = false;
let playIndex = 0;
let playTimer = 0;
let Last = Performance.now();
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
/\* ================ INIT ================ \*/
init();
loadChicken();
animate();
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x444444);
camera = new THREE.PerspectiveCamera(60, InnerWidth/innerHeight, 0.1, 1000);
camera.position.set(0,3,8);
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
transformControls = new THREE.TransformControls(camera, renderer.domElement);
transformControls.addEventListener("dragging-changed", e =\> {
Код: Выделить всё
controls.enabled = !e.value;
scene.add(transformControls);
scene.add(new THREE.AmbientLight(0xffffff,0.7));
const dirLight = new THREE.DirectionalLight(0xffffff,0.8);
dirLight.position.set(5,10,5);
scene.add(dirLight);
renderer.domElement.addEventListener("pointerdown", onPointerDown);
document.addEventListener("mousemove", e =\> {
Код: Выделить всё
mouse.x = (e.clientX / innerWidth) \\\* 2 - 1;
mouse.y = -(e.clientY / innerHeight) \\\* 2 + 1;
window.addEventListener("resize", () =\> {
Код: Выделить всё
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
/\* ================ ЗАГРУЗИТЬ КУРИЦУ ================= \*/
function loadChicken() {
const loader = new THREE.FBXLoader();
const texLoader = new THREE.TextureLoader();
loader.load(
Код: Выделить всё
"https://itswardengod.github.io/wardengame/Chicken.FBX",
obj =\\\> {
chicken = obj;
chicken.scale.setScalar(0.03);
const mainTex = texLoader.load("https://itswardengod.github.io/wardengame/Main.png");
const opacityTex = texLoader.load("https://itswardengod.github.io/wardengame/Opacity.png");
obj.traverse(child =\\\> {
if(child.isMesh) {
child.material.map = mainTex;
child.material.alphaMap = opacityTex;
child.material.alphaTest = 0.5;
child.material.side = THREE.DoubleSide;
}
if(child.isBone) {
bones.push(child);
restPose.push(child.quaternion.clone());
createBoneDot(child);
}
});
scene.add(chicken);
}
/\* ================ BONE DOTS ================ \*/
function createBoneDot(bone) {
const spriteMat = new THREE.SpriteMaterial({ color:0xff4444, deepTest:false,lengthWrite:false });
const sprite = new THREE.Sprite(spriteMat);
sprite.scale.set(1,1,1);
sprite.renderOrder = 999;
bone.add(sprite);
boneDots.push({bone,sprite});
}
/\* ================= ВЫБОР ================ \*/
function onPointerDown(e) {
raycaster.setFromCamera(мышь,камера);
const hits = raycaster.intersectObjects(boneDots.map(b=\>b.sprite));
if(hits.length) {
Код: Выделить всё
transformControls.attach(
boneDots.find(b=\\\>b.sprite===hits\\\[0\\\].object).bone
);
/\* ================ FRAMES ================ \*/
function snapshot() {
returnbones.map(b=\>b.quaternion.clone());
function saveFrame() {
undoStack.push({frames:\[...frames\], times:\[...frameTimes\]});
redoStack.length = 0;
frames.push(snapshot());
frameTimes.push(parseFloat(frameTime.value)||0.4);
rebuildTimeline();
function nextFrame() {
if(!frames.length) return;
frames\[frames.length-1\].forEach((q,i)=\>bones\[i\].quaternion.copy(q));
function rebuildTimeline() {
timeline.innerHTML="";
frames.forEach((\_,i)=\>{
Код: Выделить всё
const d=document.createElement("div");
d.className="frame";
d.textContent=i;
d.onclick=()=\\\>frames\\\[i\\\].forEach((q,b)=\\\>bones\\\[b\\\].quaternion.copy(q));
timeline.appendChild(d);
/\* ================ UNDO / REDO ================= \*/
function undo() {
if(!undoStack.length) return;
redoStack.push({frames:\[...frames\], times:\[...frameTimes\]});
const s = undoStack.pop();
frames = s.frames;
frameTimes = s.times;
rebuildTimeline();
function redo() {
if(!redoStack.length) return;
undoStack.push({frames:\[...frames\], times:\[...frameTimes\]});
const s = redoStack.pop();
frames = s.frames;
frameTimes = s.times;
rebuildTimeline();
/\* ================= RESET ================ \*/
function resetRig() {
bones.forEach((b,i)=\>b.quaternion.copy(restPose\[i\]));
/\* ================= ANIMATION ================ \*/
function playAnimation() {
if(frames.length \< 2) return;
playing = true;
playIndex = 0;
playTimer = 0;
function updateAnimation(dt) {
if(!playing) return;
playTimer += dt;
const dur =frameTimes\[playIndex\];
if(playTimer \>= dur) {
Код: Выделить всё
playTimer = 0;
playIndex++;
if(playIndex \\\>= frames.length-1) {
playing = false;
return;
}
const a = playTimer / dur;
bones.forEach((b,i)=\>{
Код: Выделить всё
b.quaternion.slerpQuaternions(
frames\\\[playIndex\\\]\\\[i\\\],
frames\\\[playIndex+1\\\]\\\[i\\\],
a
);
/\* ================ EXPORT ================ \*/
function ExportGLTF() {
const track=\[\];
let t=0;
const times=\[0\];
frameTimes.forEach(d=\>{t+=d;times.push(t);});
bones.forEach((bone,i)=\>{
Код: Выделить всё
const values=\\\[\\\];
frames.forEach(f=\\\>{
const q=f\\\[i\\\];
values.push(q.x,q.y,q.z,q.w);
});
tracks.push(new THREE.QuaternionKeyframeTrack(
bone.name+".quaternion", times, values
));
const clip = new THREE.AnimationClip("RigAnimation",-1,tracks);
chicken.animations=\[clip\];
new THREE.GLTFExporter().parse(chicken,g=\>{
Код: Выделить всё
const a=document.createElement("a");
a.href=URL.createObjectURL(new Blob(\\\[JSON.stringify(g)\\\],{type:"application/json"}));
a.download="rigged_animation.gltf";
a.click();
/\* ================ LOOP =============== \*/
function animate() {
requestAnimationFrame(animate);
const now= Performance.now();
const dt=(now-last)/1000;
last=now;
updateAnimation(dt);
controls.update();
renderer.render(сцена, камера);
}
функция setMode(m){ TransformControls.setMode(m);
\
\
\
Подробнее здесь: https://stackoverflow.com/questions/798 ... he-older-v
Мобильная версия