Chicken Rig Редактор
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 Воспроизвести Экспортировать GLTF
позволить сцене, камере, рендереру;
позволить элементам управления, TransformControls;
let Курица;
let костей = [];
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();
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, InternalHeight);
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.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));
/* ================ 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));
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
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 Воспроизвести Экспортировать GLTF
/* ================ ГЛОБАЛЬНЫЕ ================= */
let Scene, Camera, Render;
let Controls, 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.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 ================= */
функция 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));
/* ================= 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;
}
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(сцена, камера);
function setMode(m){ TransformControls.setMode(m);
Подробнее здесь: https://stackoverflow.com/questions/798 ... assistance
Мобильная версия