最近迷上了《碧蓝航线》手游,里面的SD小人超呆萌可爱,想研究下是用dragonbones还是spine来做的
在各种找教程无意间发现贴吧大佬拆包的资源,发现.skel文件里面用的是spine v3.6.52版本,于是去https://github.com/EsotericSoftware/spine-runtimes/找了3.6版本照着官方webgl案例,真的可以加载模型
不过不想自个拆包,毕竟小黄鸡更新那么频繁,皮肤有多,维护起来麻烦,所以又去github找到大佬的拆包资源
于是参考了:https://github.com/Pelom777/AzurLaneSD
改成了vue版本的,并把角色名称拼音换成中文,瓜游的模型文件命名用拼音,捣鼓了好久才用 node-pinyin
把拼音改成了中文
Demo:https://www.timelessq.com/tool/spine
仅用于学习,勿作商业用途,侵权请联系删除
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spine</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<link rel="stylesheet" href="index.css">
</head>
<body>
<div id="app" element-loading-text="拼命加载中" element-loading-background="rgba(0, 0, 0, 0.1)">
<div class="spine-tool">
<el-select v-model="seleteSkeleton" filterable :loading="listLoading" size="medium" :filter-method="filterSkel" @change="onChangeSkel">
<el-option
v-for="(item, index) in filterOptions"
:key="index"
:label="item.name"
:value="item.value"
>
<span style="float: left">{{ item.jp ? `${item.name}(${item.jp})` : item.name }}</span>
<span style="float: right; color: #606266; font-size: 12px">{{ item.remark }}</span>
</el-option>
</el-select>
<el-select v-model="selectAnimation" size="medium" @change="onChangeAnimation">
<el-option
v-for="item in animationOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<canvas id="canvas" class="spine-canvas"></canvas>
<div v-loading="skelLoading" element-loading-text="拼命加载中" element-loading-background="rgba(0, 0, 0, 0)" class="spine-loading" />
<div class="spine-scale">
<el-slider v-model="spineScale" :step="0.01" :min="0.5" :max="1.5" @input="onScaleChange" />
</div>
</div>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.21.1/dist/axios.min.js"></script>
<script src="https://pelom.gitee.io/azurlanesd/src/build/spine-webgl.js"></script>
<script src="index.js"></script>
</body>
</html>
index.css
body{
margin: 0;
}
.spine{
position: fixed;
width: 100%;
height: 100%;
}
.spine-tool{
position: fixed;
top: 30px;
left: 0;
width: 100%;
text-align: center;
}
.spine-tool .el-select{
margin-bottom: 10px;
}
.spine-scale{
position: fixed;
bottom: 30px;
left: 50%;
width: 410px;
margin-left: -205px;
}
.spine-loading {
position: fixed;
left: 50%;
bottom: 90px;
width: 100px;
height: 60px;
margin-left: -50px;
}
@media (max-width: 576px) {
.spine-tool {
top: 15px;
}
.spine-scale{
left: 0;
bottom: 15px;
width: 100%;
padding: 0 15px;
margin-left: 0;
}
.spine-loading{
bottom: 75px;
}
}
index.js
var app = new Vue({
el: '#app',
data() {
return {
seleteSkeleton: 'lafei_4',
skelOptions: [],
listLoading: false,
filterOptions: [],
selectAnimation: '',
animationOptions: [],
checkboxGroup1: '',
spineScale: 1,
skelLoading: false
}
},
mounted() {
this.initSpine()
this.fetchList()
},
methods: {
async fetchList() {
this.listLoading = true
await axios.get('//api.timelessq.com/spine/lists').then(res => {
this.filterOptions = this.skelOptions = res.data.data
}).catch(() => {})
this.listLoading = false
},
fetchAssets() {
return axios.get('//api.timelessq.com/spine', {
params: {
id: this.seleteSkeleton,
isuseCDN: true
}
}).then(res => {
this.assetsData = res.data.data
this.loadAsset()
})
},
async initSpine() {
// Setup canvas and WebGL context. We pass alpha: false to canvas.getContext() so we don't use premultiplied alpha when
// loading textures. That is handled separately by PolygonBatcher.
this.spineCanvas = document.getElementById('canvas')
console.log(window.innerWidth)
this.spineCanvas.width = window.innerWidth
this.spineCanvas.height = window.innerHeight
const config = { alpha: false }
this.gl = this.spineCanvas.getContext('webgl', config) || this.spineCanvas.getContext('experimental-webgl', config)
if (!this.gl) {
alert('WebGL is unavailable.')
return
}
// Create a simple shader, mesh, model-view-projection matrix, SkeletonRenderer, and AssetManager.
this.shader = spine.webgl.Shader.newTwoColoredTextured(this.gl)
this.batcher = new spine.webgl.PolygonBatcher(this.gl)
this.mvp = new spine.webgl.Matrix4()
this.mvp.ortho2d(0, 0, this.spineCanvas.width - 1, this.spineCanvas.height - 1)
this.skeletonRenderer = new spine.webgl.SkeletonRenderer(this.gl)
this.assetManager = new spine.webgl.AssetManager(this.gl)
await this.fetchAssets()
requestAnimationFrame(this.load)
},
loadAsset() {
const { atlas, skelBinary, skelJson } = this.assetsData
if (skelJson) {
this.assetManager.loadText(skelJson)
} else {
this.assetManager.loadBinary(skelBinary)
}
this.assetManager.loadTextureAtlas(atlas)
},
load() {
this.skelLoading = true
// Wait until the AssetManager has loaded all resources, then load the skeletons.
if (this.assetManager.isLoadingComplete()) {
this.selectAnimation = 'normal'
this.activeSkeleton = this.loadSkeleton(false)
this.isChange = false
this.lastFrameTime = Date.now() / 1000
this.setupAnimation()
requestAnimationFrame(this.render)
} else {
requestAnimationFrame(this.load)
}
},
loadSkeleton(premultipliedAlpha, skin) {
const { atlas, skelBinary, skelJson } = this.assetsData
if (skin === undefined) skin = 'default'
// Load the texture atlas using name.atlas from the AssetManager.
const atlasData = this.assetManager.get(atlas)
// Create a AtlasAttachmentLoader that resolves region, mesh, boundingbox and path attachments
const atlasLoader = new spine.AtlasAttachmentLoader(atlasData)
// Set the scale to apply during parsing, parse the file, and create a new skeleton.
let skeletonData
if (skelJson) {
var skeletonJson = new spine.SkeletonJson(atlasLoader)
skeletonData = skeletonJson.readSkeletonData(this.assetManager.get(skelJson))
} else {
// Create a SkeletonBinary instance for parsing the .skel file.
// var skeletonBinary = new spine.SkeletonBinary(atlasLoader);
var skeletonBinary = new spine.SkeletonBinary(atlasLoader)
skeletonData = skeletonBinary.readSkeletonData(this.assetManager.get(skelBinary))
}
const skeleton = new spine.Skeleton(skeletonData)
this.root = skeletonData.findBone('root')
skeleton.setSkinByName(skin)
const bounds = this.calculateBounds(skeleton)
// // Create an AnimationState, and set the initial animation in looping mode.
const animationStateData = new spine.AnimationStateData(skeleton.data)
const animationState = new spine.AnimationState(animationStateData)
if (skeleton.data.findAnimation(this.selectAnimation) == null) {
this.selectAnimation = skeleton.data.animations[0].name
}
animationState.setAnimation(0, this.selectAnimation, true)
// Pack everything up and return to caller.
return { skeleton, state: animationState, bounds, premultipliedAlpha }
},
calculateBounds(skeleton) {
skeleton.setToSetupPose()
skeleton.updateWorldTransform()
const offset = new spine.Vector2()
const size = new spine.Vector2()
skeleton.getBounds(offset, size, [])
return { offset, size }
},
render() {
this.skelLoading = false
if (this.isChange) {
this.load()
return
}
const now = Date.now() / 1000
const delta = now - this.lastFrameTime
this.lastFrameTime = now
// Update the MVP matrix to adjust for canvas size changes
this.resize()
this.gl.clearColor(0.5, 0.5, 0.5, 1)
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
// Apply the animation state based on the delta time.
const state = this.activeSkeleton.state
const skeleton = this.activeSkeleton.skeleton
const premultipliedAlpha = this.activeSkeleton.premultipliedAlpha
state.update(delta)
state.apply(skeleton)
skeleton.updateWorldTransform()
// Bind the shader and set the texture and model-view-projection matrix.
this.shader.bind()
this.shader.setUniformi(spine.webgl.Shader.SAMPLER, 0)
this.shader.setUniform4x4f(spine.webgl.Shader.MVP_MATRIX, this.mvp.values)
// Start the batch and tell the SkeletonRenderer to render the active skeleton.
this.batcher.begin(this.shader)
this.skeletonRenderer.premultipliedAlpha = premultipliedAlpha
this.skeletonRenderer.draw(this.batcher, skeleton)
this.batcher.end()
this.shader.unbind()
requestAnimationFrame(this.render)
},
resize() {
const w = this.spineCanvas.clientWidth
const h = this.spineCanvas.clientHeight
const bounds = this.activeSkeleton.bounds
if (this.spineCanvas.width !== w || this.spineCanvas.height !== h) {
this.spineCanvas.width = w
this.spineCanvas.height = h
}
// magic
const centerX = bounds.offset.x + bounds.size.x / 2
const centerY = bounds.offset.y + bounds.size.y / 2
const scaleX = bounds.size.x / this.spineCanvas.width
const scaleY = bounds.size.y / this.spineCanvas.height
let scale = Math.max(scaleX, scaleY) * 1.2
if (scale < 1) scale = 1
const width = this.spineCanvas.width * scale
const height = this.spineCanvas.height * scale
this.mvp.ortho2d(centerX - width / 2, centerY - height / 2, width, height)
this.gl.viewport(0, 0, this.spineCanvas.width, this.spineCanvas.height)
},
setupAnimation() {
const skeleton = this.activeSkeleton.skeleton
this.animationOptions = skeleton.data.animations.map(item => {
return {
label: item.name,
value: item.name
}
})
},
async onChangeSkel() {
await this.fetchAssets()
this.isChange = true
},
onChangeAnimation(animationName) {
const state = this.activeSkeleton.state
const skeleton = this.activeSkeleton.skeleton
skeleton.setToSetupPose()
state.setAnimation(0, animationName, true)
},
onScaleChange(val) {
if (!this.root) return
this.root.scaleX = this.root.scaleY = val
},
filterSkel(keyword) {
if (keyword) {
this.filterOptions = this.skelOptions.filter(item => JSON.stringify(item).toLowerCase().includes(keyword))
} else {
this.filterOptions = this.skelOptions
}
}
}
})
参考资料:spine-runtimes/spine-ts at 3.6 · EsotericSoftware/spine-runtimes
还没有评论,快来抢第一吧