Commit 0864007f authored by wjf's avatar wjf

l

parent e3424da3
This diff is collapsed.
This diff is collapsed.
......@@ -86,7 +86,7 @@ export class D3Renderer extends ObjectRenderer {
let mat: BaseMaterial = mesh.material;
let geo = mesh.geometry;
let shader = getCusShader(this.renderer, mat, this.lightsConfig);
let shader = getCusShader(this.renderer, mat, this.lightsConfig,mesh);
if (curShader !== shader) {//同一着色器只需要传一次相机参数和光照参数(因为都是场景全局)
curShader = shader;
//先绑定着色器,project不设置
......@@ -226,7 +226,7 @@ export class D3Renderer extends ObjectRenderer {
//变形的数据可能传入的会改变,所以需要upload,还要重新修改数据,有removeAttribute的情况,TODO
//还得在这里做数据的变形,不绑定
if (uploadBufferDatas) uploadBufferDatas.forEach((e) => {
e.buffer.upload(e.data, 0, true);
e.buffer.upload(e.data, 0, false/*true*/);//貌似里面有修改的Attribute队列,所以还是绑定下buffer
})
}
//根据材质切换渲染面
......
This diff is collapsed.
import { AnimationUtils } from './AnimationUtils.js';
import { KeyframeTrack } from './KeyframeTrack.js';
import { BooleanKeyframeTrack } from './tracks/BooleanKeyframeTrack.js';
import { ColorKeyframeTrack } from './tracks/ColorKeyframeTrack.js';
import { NumberKeyframeTrack } from './tracks/NumberKeyframeTrack.js';
import { QuaternionKeyframeTrack } from './tracks/QuaternionKeyframeTrack.js';
import { StringKeyframeTrack } from './tracks/StringKeyframeTrack.js';
import { VectorKeyframeTrack } from './tracks/VectorKeyframeTrack.js';
import { _Math } from '../math/Math.js';
/**
*
* Reusable set of Tracks that represent an animation.
*
* @author Ben Houston / http://clara.io/
* @author David Sarno / http://lighthaus.us/
*/
function AnimationClip( name, duration, tracks ) {
this.name = name;
this.tracks = tracks;
this.duration = ( duration !== undefined ) ? duration : - 1;
this.uuid = _Math.generateUUID();
// this means it should figure out its duration by scanning the tracks
if ( this.duration < 0 ) {
this.resetDuration();
}
}
function getTrackTypeForValueTypeName( typeName ) {
switch ( typeName.toLowerCase() ) {
case 'scalar':
case 'double':
case 'float':
case 'number':
case 'integer':
return NumberKeyframeTrack;
case 'vector':
case 'vector2':
case 'vector3':
case 'vector4':
return VectorKeyframeTrack;
case 'color':
return ColorKeyframeTrack;
case 'quaternion':
return QuaternionKeyframeTrack;
case 'bool':
case 'boolean':
return BooleanKeyframeTrack;
case 'string':
return StringKeyframeTrack;
}
throw new Error( 'THREE.KeyframeTrack: Unsupported typeName: ' + typeName );
}
function parseKeyframeTrack( json ) {
if ( json.type === undefined ) {
throw new Error( 'THREE.KeyframeTrack: track type undefined, can not parse' );
}
var trackType = getTrackTypeForValueTypeName( json.type );
if ( json.times === undefined ) {
var times = [], values = [];
AnimationUtils.flattenJSON( json.keys, times, values, 'value' );
json.times = times;
json.values = values;
}
// derived classes can define a static parse method
if ( trackType.parse !== undefined ) {
return trackType.parse( json );
} else {
// by default, we assume a constructor compatible with the base
return new trackType( json.name, json.times, json.values, json.interpolation );
}
}
Object.assign( AnimationClip, {
parse: function ( json ) {
var tracks = [],
jsonTracks = json.tracks,
frameTime = 1.0 / ( json.fps || 1.0 );
for ( var i = 0, n = jsonTracks.length; i !== n; ++ i ) {
tracks.push( parseKeyframeTrack( jsonTracks[ i ] ).scale( frameTime ) );
}
return new AnimationClip( json.name, json.duration, tracks );
},
toJSON: function ( clip ) {
var tracks = [],
clipTracks = clip.tracks;
var json = {
'name': clip.name,
'duration': clip.duration,
'tracks': tracks,
'uuid': clip.uuid
};
for ( var i = 0, n = clipTracks.length; i !== n; ++ i ) {
tracks.push( KeyframeTrack.toJSON( clipTracks[ i ] ) );
}
return json;
},
CreateFromMorphTargetSequence: function ( name, morphTargetSequence, fps, noLoop ) {
var numMorphTargets = morphTargetSequence.length;
var tracks = [];
for ( var i = 0; i < numMorphTargets; i ++ ) {
var times = [];
var values = [];
times.push(
( i + numMorphTargets - 1 ) % numMorphTargets,
i,
( i + 1 ) % numMorphTargets );
values.push( 0, 1, 0 );
var order = AnimationUtils.getKeyframeOrder( times );
times = AnimationUtils.sortedArray( times, 1, order );
values = AnimationUtils.sortedArray( values, 1, order );
// if there is a key at the first frame, duplicate it as the
// last frame as well for perfect loop.
if ( ! noLoop && times[ 0 ] === 0 ) {
times.push( numMorphTargets );
values.push( values[ 0 ] );
}
tracks.push(
new NumberKeyframeTrack(
'.morphTargetInfluences[' + morphTargetSequence[ i ].name + ']',
times, values
).scale( 1.0 / fps ) );
}
return new AnimationClip( name, - 1, tracks );
},
findByName: function ( objectOrClipArray, name ) {
var clipArray = objectOrClipArray;
if ( ! Array.isArray( objectOrClipArray ) ) {
var o = objectOrClipArray;
clipArray = o.geometry && o.geometry.animations || o.animations;
}
for ( var i = 0; i < clipArray.length; i ++ ) {
if ( clipArray[ i ].name === name ) {
return clipArray[ i ];
}
}
return null;
},
CreateClipsFromMorphTargetSequences: function ( morphTargets, fps, noLoop ) {
var animationToMorphTargets = {};
// tested with https://regex101.com/ on trick sequences
// such flamingo_flyA_003, flamingo_run1_003, crdeath0059
var pattern = /^([\w-]*?)([\d]+)$/;
// sort morph target names into animation groups based
// patterns like Walk_001, Walk_002, Run_001, Run_002
for ( var i = 0, il = morphTargets.length; i < il; i ++ ) {
var morphTarget = morphTargets[ i ];
var parts = morphTarget.name.match( pattern );
if ( parts && parts.length > 1 ) {
var name = parts[ 1 ];
var animationMorphTargets = animationToMorphTargets[ name ];
if ( ! animationMorphTargets ) {
animationToMorphTargets[ name ] = animationMorphTargets = [];
}
animationMorphTargets.push( morphTarget );
}
}
var clips = [];
for ( var name in animationToMorphTargets ) {
clips.push( AnimationClip.CreateFromMorphTargetSequence( name, animationToMorphTargets[ name ], fps, noLoop ) );
}
return clips;
},
// parse the animation.hierarchy format
parseAnimation: function ( animation, bones ) {
if ( ! animation ) {
console.error( 'THREE.AnimationClip: No animation in JSONLoader data.' );
return null;
}
var addNonemptyTrack = function ( trackType, trackName, animationKeys, propertyName, destTracks ) {
// only return track if there are actually keys.
if ( animationKeys.length !== 0 ) {
var times = [];
var values = [];
AnimationUtils.flattenJSON( animationKeys, times, values, propertyName );
// empty keys are filtered out, so check again
if ( times.length !== 0 ) {
destTracks.push( new trackType( trackName, times, values ) );
}
}
};
var tracks = [];
var clipName = animation.name || 'default';
// automatic length determination in AnimationClip.
var duration = animation.length || - 1;
var fps = animation.fps || 30;
var hierarchyTracks = animation.hierarchy || [];
for ( var h = 0; h < hierarchyTracks.length; h ++ ) {
var animationKeys = hierarchyTracks[ h ].keys;
// skip empty tracks
if ( ! animationKeys || animationKeys.length === 0 ) continue;
// process morph targets
if ( animationKeys[ 0 ].morphTargets ) {
// figure out all morph targets used in this track
var morphTargetNames = {};
for ( var k = 0; k < animationKeys.length; k ++ ) {
if ( animationKeys[ k ].morphTargets ) {
for ( var m = 0; m < animationKeys[ k ].morphTargets.length; m ++ ) {
morphTargetNames[ animationKeys[ k ].morphTargets[ m ] ] = - 1;
}
}
}
// create a track for each morph target with all zero
// morphTargetInfluences except for the keys in which
// the morphTarget is named.
for ( var morphTargetName in morphTargetNames ) {
var times = [];
var values = [];
for ( var m = 0; m !== animationKeys[ k ].morphTargets.length; ++ m ) {
var animationKey = animationKeys[ k ];
times.push( animationKey.time );
values.push( ( animationKey.morphTarget === morphTargetName ) ? 1 : 0 );
}
tracks.push( new NumberKeyframeTrack( '.morphTargetInfluence[' + morphTargetName + ']', times, values ) );
}
duration = morphTargetNames.length * ( fps || 1.0 );
} else {
// ...assume skeletal animation
var boneName = '.bones[' + bones[ h ].name + ']';
addNonemptyTrack(
VectorKeyframeTrack, boneName + '.position',
animationKeys, 'pos', tracks );
addNonemptyTrack(
QuaternionKeyframeTrack, boneName + '.quaternion',
animationKeys, 'rot', tracks );
addNonemptyTrack(
VectorKeyframeTrack, boneName + '.scale',
animationKeys, 'scl', tracks );
}
}
if ( tracks.length === 0 ) {
return null;
}
var clip = new AnimationClip( clipName, duration, tracks );
return clip;
}
} );
Object.assign( AnimationClip.prototype, {
resetDuration: function () {
var tracks = this.tracks, duration = 0;
for ( var i = 0, n = tracks.length; i !== n; ++ i ) {
var track = this.tracks[ i ];
duration = Math.max( duration, track.times[ track.times.length - 1 ] );
}
this.duration = duration;
return this;
},
trim: function () {
for ( var i = 0; i < this.tracks.length; i ++ ) {
this.tracks[ i ].trim( 0, this.duration );
}
return this;
},
validate: function () {
var valid = true;
for ( var i = 0; i < this.tracks.length; i ++ ) {
valid = valid && this.tracks[ i ].validate();
}
return valid;
},
optimize: function () {
for ( var i = 0; i < this.tracks.length; i ++ ) {
this.tracks[ i ].optimize();
}
return this;
}
} );
export { AnimationClip };
import { AnimationTrack3D } from "./AnimationTrack3D";
import { HashObject } from "../../2d/HashObject";
import { EventDispatcher, Event } from "../../2d/events";
/**
* 不需要挂到节点上
* 暂时没有帧,只有时间,以后再说
* 用update更新
*/
export class AnimationClip3D extends EventDispatcher {
/**
* 所有的动画数据
*/
private tracks: AnimationTrack3D[];
private _totalTime: number;
get totalTime() {
return this._totalTime;
}
constructor(tracks: AnimationTrack3D[]) {
super()
this._instanceType = "AnimationClip"
this.tracks = tracks;
this.calculateTotalTime();
}
private calculateTotalTime() {
var tracks = this.tracks, duration = 0;
for (var i = 0, n = tracks.length; i !== n; ++i) {
var track = this.tracks[i];
duration = Math.max(duration, track.times[track.times.length - 1]);
}
this._totalTime = duration;
return this;
}
private _isPlaying: boolean = true;
public get isPlaying(): boolean {
return this._isPlaying;
}
private _isFront: boolean = true;
get isFront(): boolean {
return this._isFront;
}
/**
* 上个时间,用来确定是否更新
*/
private lastTime: number = null;
/**
* 记录时间
*/
private curTime: number = 0;
/**
* 需要挂在循环里的方法,传时间间隔
* @param time
*/
update(time: number) {
if (!this.tracks || !this.tracks.length) return;
//时间不等,直接播放
if (this.curTime !== this.lastTime) {
this.rectify()
return;
}
//时间没有,或没在播放
if (time <= 0 || !this._isPlaying) return;
if (this._isFront) {
this.curTime += time;
if (this.curTime > this._totalTime) this.curTime = 0;
} else {
this.curTime -= time;
if (this.curTime < 0) this.curTime = this._totalTime;
}
if (this.curTime !== this.lastTime) {
//矫正
this.rectify();
//派发事件
this.dispatchEvent(Event.ENTER_FRAME);
}
}
/**
* 从当前时间点播放
* @param isFront 默认true正向
*/
play(isFront: boolean = true) {
this._isFront = isFront;
this._isPlaying = true;
}
/**
* 停在当前时间
*/
stop() {
this._isPlaying = false;
}
/**
*
* @param time
* @param isFront 默认true,正向播放
*/
public gotoAndPlay(time: number, isFront: boolean = true): void {
let s = this;
s._isFront = isFront;
s._isPlaying = true;
if (time > s._totalTime) time = s._totalTime;
if (time < 1) time = 0;
s.curTime = time;
}
/**
* 停在指定时间
* @param time
*/
public gotoAndStop(time: number): void {
this._isPlaying = false;
if (time > this.totalTime) time = this.totalTime;
if (time < 0) time = 0;
this.curTime = time;;
}
private startAniRangeFun;
public startAniRange(
beginTime: number = 0,
endTime: number = this._totalTime,
loops: number = 1,
callback?: Function
) {
if (beginTime < 0) beginTime = 0;
if (beginTime > this._totalTime) beginTime = this._totalTime;
if (endTime < 0) endTime = 0;
if (endTime > this._totalTime) endTime = this._totalTime;
if (beginTime === endTime) {
this.gotoAndStop(beginTime)
//如果相等
return
} else if (beginTime < endTime) {
this._isFront = true;
} else {
this._isFront = false;
var temp = beginTime;
beginTime = endTime;
endTime = temp;
}
//移除原先的绑定吧
if (this.startAniRangeFun) this.removeEventListener(Event.ENTER_FRAME, this.startAniRangeFun, this)
this.curTime = beginTime;
this._isPlaying = true;
let loopCount = loops ? (loops + 0.5 >> 0) : Infinity;
this.addEventListener(Event.ENTER_FRAME, this.startAniRangeFun = (e: Event) => {
let s: AnimationClip3D = e.target;
if (s._isFront) {
if (s.curTime >= endTime) {
loopCount--;
if (loopCount <= 0) {
s.gotoAndStop(endTime);
s.removeEventListener(Event.ENTER_FRAME, this.startAniRangeFun, this);
this.startAniRangeFun = null;
callback && callback();
} else {
s.gotoAndPlay(beginTime);
}
}
} else {
if (s.curTime <= beginTime) {
loopCount--
if (loopCount <= 0) {
s.gotoAndStop(beginTime);
s.removeEventListener(Event.ENTER_FRAME, this.startAniRangeFun, this);
this.startAniRangeFun = null;
callback && callback();
} else {
s.gotoAndPlay(endTime, false);
}
}
}
}, this)
}
/**
* 矫正
*/
private rectify() {
if (!this.tracks || !this.tracks.length) return;
for (var i = 0; i < this.tracks.length; i++) {
this.tracks[i].setValue(this.curTime)
}
//设置相等
this.lastTime = this.curTime;
}
}
\ No newline at end of file
This diff is collapsed.
import { PropertyBinding } from './PropertyBinding.js';
import { _Math } from '../math/Math.js';
/**
*
* A group of objects that receives a shared animation state.
*
* Usage:
*
* - Add objects you would otherwise pass as 'root' to the
* constructor or the .clipAction method of AnimationMixer.
*
* - Instead pass this object as 'root'.
*
* - You can also add and remove objects later when the mixer
* is running.
*
* Note:
*
* Objects of this class appear as one object to the mixer,
* so cache control of the individual objects must be done
* on the group.
*
* Limitation:
*
* - The animated properties must be compatible among the
* all objects in the group.
*
* - A single property can either be controlled through a
* target group or directly, but not both.
*
* @author tschw
*/
function AnimationObjectGroup() {
this.uuid = _Math.generateUUID();
// cached objects followed by the active ones
this._objects = Array.prototype.slice.call( arguments );
this.nCachedObjects_ = 0; // threshold
// note: read by PropertyBinding.Composite
var indices = {};
this._indicesByUUID = indices; // for bookkeeping
for ( var i = 0, n = arguments.length; i !== n; ++ i ) {
indices[ arguments[ i ].uuid ] = i;
}
this._paths = []; // inside: string
this._parsedPaths = []; // inside: { we don't care, here }
this._bindings = []; // inside: Array< PropertyBinding >
this._bindingsIndicesByPath = {}; // inside: indices in these arrays
var scope = this;
this.stats = {
objects: {
get total() {
return scope._objects.length;
},
get inUse() {
return this.total - scope.nCachedObjects_;
}
},
get bindingsPerObject() {
return scope._bindings.length;
}
};
}
Object.assign( AnimationObjectGroup.prototype, {
isAnimationObjectGroup: true,
add: function () {
var objects = this._objects,
nObjects = objects.length,
nCachedObjects = this.nCachedObjects_,
indicesByUUID = this._indicesByUUID,
paths = this._paths,
parsedPaths = this._parsedPaths,
bindings = this._bindings,
nBindings = bindings.length,
knownObject = undefined;
for ( var i = 0, n = arguments.length; i !== n; ++ i ) {
var object = arguments[ i ],
uuid = object.uuid,
index = indicesByUUID[ uuid ];
if ( index === undefined ) {
// unknown object -> add it to the ACTIVE region
index = nObjects ++;
indicesByUUID[ uuid ] = index;
objects.push( object );
// accounting is done, now do the same for all bindings
for ( var j = 0, m = nBindings; j !== m; ++ j ) {
bindings[ j ].push( new PropertyBinding( object, paths[ j ], parsedPaths[ j ] ) );
}
} else if ( index < nCachedObjects ) {
knownObject = objects[ index ];
// move existing object to the ACTIVE region
var firstActiveIndex = -- nCachedObjects,
lastCachedObject = objects[ firstActiveIndex ];
indicesByUUID[ lastCachedObject.uuid ] = index;
objects[ index ] = lastCachedObject;
indicesByUUID[ uuid ] = firstActiveIndex;
objects[ firstActiveIndex ] = object;
// accounting is done, now do the same for all bindings
for ( var j = 0, m = nBindings; j !== m; ++ j ) {
var bindingsForPath = bindings[ j ],
lastCached = bindingsForPath[ firstActiveIndex ],
binding = bindingsForPath[ index ];
bindingsForPath[ index ] = lastCached;
if ( binding === undefined ) {
// since we do not bother to create new bindings
// for objects that are cached, the binding may
// or may not exist
binding = new PropertyBinding( object, paths[ j ], parsedPaths[ j ] );
}
bindingsForPath[ firstActiveIndex ] = binding;
}
} else if ( objects[ index ] !== knownObject ) {
console.error( 'THREE.AnimationObjectGroup: Different objects with the same UUID ' +
'detected. Clean the caches or recreate your infrastructure when reloading scenes.' );
} // else the object is already where we want it to be
} // for arguments
this.nCachedObjects_ = nCachedObjects;
},
remove: function () {
var objects = this._objects,
nCachedObjects = this.nCachedObjects_,
indicesByUUID = this._indicesByUUID,
bindings = this._bindings,
nBindings = bindings.length;
for ( var i = 0, n = arguments.length; i !== n; ++ i ) {
var object = arguments[ i ],
uuid = object.uuid,
index = indicesByUUID[ uuid ];
if ( index !== undefined && index >= nCachedObjects ) {
// move existing object into the CACHED region
var lastCachedIndex = nCachedObjects ++,
firstActiveObject = objects[ lastCachedIndex ];
indicesByUUID[ firstActiveObject.uuid ] = index;
objects[ index ] = firstActiveObject;
indicesByUUID[ uuid ] = lastCachedIndex;
objects[ lastCachedIndex ] = object;
// accounting is done, now do the same for all bindings
for ( var j = 0, m = nBindings; j !== m; ++ j ) {
var bindingsForPath = bindings[ j ],
firstActive = bindingsForPath[ lastCachedIndex ],
binding = bindingsForPath[ index ];
bindingsForPath[ index ] = firstActive;
bindingsForPath[ lastCachedIndex ] = binding;
}
}
} // for arguments
this.nCachedObjects_ = nCachedObjects;
},
// remove & forget
uncache: function () {
var objects = this._objects,
nObjects = objects.length,
nCachedObjects = this.nCachedObjects_,
indicesByUUID = this._indicesByUUID,
bindings = this._bindings,
nBindings = bindings.length;
for ( var i = 0, n = arguments.length; i !== n; ++ i ) {
var object = arguments[ i ],
uuid = object.uuid,
index = indicesByUUID[ uuid ];
if ( index !== undefined ) {
delete indicesByUUID[ uuid ];
if ( index < nCachedObjects ) {
// object is cached, shrink the CACHED region
var firstActiveIndex = -- nCachedObjects,
lastCachedObject = objects[ firstActiveIndex ],
lastIndex = -- nObjects,
lastObject = objects[ lastIndex ];
// last cached object takes this object's place
indicesByUUID[ lastCachedObject.uuid ] = index;
objects[ index ] = lastCachedObject;
// last object goes to the activated slot and pop
indicesByUUID[ lastObject.uuid ] = firstActiveIndex;
objects[ firstActiveIndex ] = lastObject;
objects.pop();
// accounting is done, now do the same for all bindings
for ( var j = 0, m = nBindings; j !== m; ++ j ) {
var bindingsForPath = bindings[ j ],
lastCached = bindingsForPath[ firstActiveIndex ],
last = bindingsForPath[ lastIndex ];
bindingsForPath[ index ] = lastCached;
bindingsForPath[ firstActiveIndex ] = last;
bindingsForPath.pop();
}
} else {
// object is active, just swap with the last and pop
var lastIndex = -- nObjects,
lastObject = objects[ lastIndex ];
indicesByUUID[ lastObject.uuid ] = index;
objects[ index ] = lastObject;
objects.pop();
// accounting is done, now do the same for all bindings
for ( var j = 0, m = nBindings; j !== m; ++ j ) {
var bindingsForPath = bindings[ j ];
bindingsForPath[ index ] = bindingsForPath[ lastIndex ];
bindingsForPath.pop();
}
} // cached or active
} // if object is known
} // for arguments
this.nCachedObjects_ = nCachedObjects;
},
// Internal interface used by befriended PropertyBinding.Composite:
subscribe_: function ( path, parsedPath ) {
// returns an array of bindings for the given path that is changed
// according to the contained objects in the group
var indicesByPath = this._bindingsIndicesByPath,
index = indicesByPath[ path ],
bindings = this._bindings;
if ( index !== undefined ) return bindings[ index ];
var paths = this._paths,
parsedPaths = this._parsedPaths,
objects = this._objects,
nObjects = objects.length,
nCachedObjects = this.nCachedObjects_,
bindingsForPath = new Array( nObjects );
index = bindings.length;
indicesByPath[ path ] = index;
paths.push( path );
parsedPaths.push( parsedPath );
bindings.push( bindingsForPath );
for ( var i = nCachedObjects, n = objects.length; i !== n; ++ i ) {
var object = objects[ i ];
bindingsForPath[ i ] = new PropertyBinding( object, path, parsedPath );
}
return bindingsForPath;
},
unsubscribe_: function ( path ) {
// tells the group to forget about a property path and no longer
// update the array previously obtained with 'subscribe_'
var indicesByPath = this._bindingsIndicesByPath,
index = indicesByPath[ path ];
if ( index !== undefined ) {
var paths = this._paths,
parsedPaths = this._parsedPaths,
bindings = this._bindings,
lastBindingsIndex = bindings.length - 1,
lastBindings = bindings[ lastBindingsIndex ],
lastBindingsPath = path[ lastBindingsIndex ];
indicesByPath[ lastBindingsPath ] = index;
bindings[ index ] = lastBindings;
bindings.pop();
parsedPaths[ index ] = parsedPaths[ lastBindingsIndex ];
parsedPaths.pop();
paths[ index ] = paths[ lastBindingsIndex ];
paths.pop();
}
}
} );
export { AnimationObjectGroup };
import { HashObject } from "../../2d/HashObject";
import { Object3D } from "../Object3D";
import { Mesh3D } from "../Mesh3D";
import { clamp } from "../../2d/utils";
import { Quaternion } from "../math/Quaternion";
/**
* 动画类型
*/
export enum AnimationType {
/**
* 旋转
*/
quaternion = "quaternion",
/**
* 缩放
*/
scale = "scale",
/**
* 顶点权重
*/
morphTargetInfluences = 'morphTargetInfluences',
/**
* 位移
*/
position = "position"
}
/**
* 记录动画的轨迹,实时计算,还是用每帧的,暂时按时间间隔来搞,实时计算吧
* 统统用线性的吧,貌似时间都打过点
*/
export class AnimationTrack3D extends HashObject {
constructor(
/**
* 需要改变属性的节点对象
*/
public node: Object3D | Mesh3D,
/**
* 动画类型
*/
public animationType: AnimationType,
/**
* 记录所有时间的数组,按顺序递增,没有的按最后一个时间的数据
*/
public times: Float32Array | number[],
/**
* 上述时间要对应的数值,values.length/times.length就是步长
*/
public values: Float32Array | number[]
) {
super()
this._instanceType = "AnimationTrack";
}
/**
* 传入时间设置对应的属性值,time大于所有动画总时间,外部处理
* @param time
*/
setValue(time: number) {
var value: Float32Array | number[] = this.getValue(time);
switch (this.animationType) {
case AnimationType.position:
case AnimationType.scale:
case AnimationType.quaternion:
this.node[this.animationType].fromArray(value);
break;
case AnimationType.morphTargetInfluences:
if (this.node.instanceType == "Mesh3D") {
var arr = this.node["morphTargetInfluences"]
for (var i = 0; i < arr.length; i++) {
arr[i] = value[i] || 0;
}
}
break;
}
}
private getValue(time: number) {
var size = this.getValueSize();
//为0直接用第一帧数据
if (time <= 0) return this.values.slice(0, size);
//如果超过最后时间,给最后一帧数据
if (time >= this.times[this.times.length - 1]) return this.values.slice(-size);
//取最近的两个时间索引,
var t1 = this.findPreIndex(time);
var t0 = t1 - 1;
if (t0 < 0) return this.values.slice(0, size);
//计算比例插值
var scale = (time - this.times[t0]) / (this.times[t1] - this.times[t0]);
//获取数组,
var v0 = this.values.slice(t0 * size, t0 * size + size);
var v1 = this.values.slice(t1 * size, t1 * size + size);
// time = clamp(time, 0, this.times[this.times.length - 1]);
//暂时都是线性的,到时再说
if (this.animationType == AnimationType.quaternion) {
return quaternionLinearInterpolate(scale, v0, v1);
}
return linearInterpolate(scale, v0, v1);
}
//每项尺寸
private getValueSize() {
return this.values.length / this.times.length;
}
/**
* 外部已经剔除了第一帧和最后一帧
* @param time
*/
private findPreIndex(time: number) {
var lastIndex = 0;
for (var i = 0; i < this.times.length; i++) {
if (this.times[i] > time) {
lastIndex = i;
break
}
}
return lastIndex;
}
destroy() {
this.node = null;
this.values = null;
this.times = null;
}
}
//线性插值,除了四元数
function linearInterpolate(s: number, v0: Float32Array | number[], v1: Float32Array | number[]) {
var result = [];
// values = this.sampleValues,
// stride = this.valueSize,
// offset1 = i1 * stride,
// offset0 = offset1 - stride,
// weight1 = (t - t0) / (t1 - t0),
// weight0 = 1 - weight1;
for (var i = 0; i !== v0.length; ++i) {
// result[i] =
// values[offset0 + i] * weight0 +
// values[offset1 + i] * weight1;
result[i] = v0[i] * (1 - s) + v1[i] * s;
}
return result;
}
function quaternionLinearInterpolate(s: number, v0: Float32Array | number[], v1: Float32Array | number[]) {
var result = [];
Quaternion.slerpFlat(result, 0, v0, 0, v1, 0, s);
return result;
}
\ No newline at end of file
/**
* @author tschw
* @author Ben Houston / http://clara.io/
* @author David Sarno / http://lighthaus.us/
*/
var AnimationUtils = {
// same as Array.prototype.slice, but also works on typed arrays
arraySlice: function ( array, from, to ) {
if ( AnimationUtils.isTypedArray( array ) ) {
// in ios9 array.subarray(from, undefined) will return empty array
// but array.subarray(from) or array.subarray(from, len) is correct
return new array.constructor( array.subarray( from, to !== undefined ? to : array.length ) );
}
return array.slice( from, to );
},
// converts an array to a specific type
convertArray: function ( array, type, forceClone ) {
if ( ! array || // let 'undefined' and 'null' pass
! forceClone && array.constructor === type ) return array;
if ( typeof type.BYTES_PER_ELEMENT === 'number' ) {
return new type( array ); // create typed array
}
return Array.prototype.slice.call( array ); // create Array
},
isTypedArray: function ( object ) {
return ArrayBuffer.isView( object ) &&
! ( object instanceof DataView );
},
// returns an array by which times and values can be sorted
getKeyframeOrder: function ( times ) {
function compareTime( i, j ) {
return times[ i ] - times[ j ];
}
var n = times.length;
var result = new Array( n );
for ( var i = 0; i !== n; ++ i ) result[ i ] = i;
result.sort( compareTime );
return result;
},
// uses the array previously returned by 'getKeyframeOrder' to sort data
sortedArray: function ( values, stride, order ) {
var nValues = values.length;
var result = new values.constructor( nValues );
for ( var i = 0, dstOffset = 0; dstOffset !== nValues; ++ i ) {
var srcOffset = order[ i ] * stride;
for ( var j = 0; j !== stride; ++ j ) {
result[ dstOffset ++ ] = values[ srcOffset + j ];
}
}
return result;
},
// function for parsing AOS keyframe formats
flattenJSON: function ( jsonKeys, times, values, valuePropertyName ) {
var i = 1, key = jsonKeys[ 0 ];
while ( key !== undefined && key[ valuePropertyName ] === undefined ) {
key = jsonKeys[ i ++ ];
}
if ( key === undefined ) return; // no data
var value = key[ valuePropertyName ];
if ( value === undefined ) return; // no data
if ( Array.isArray( value ) ) {
do {
value = key[ valuePropertyName ];
if ( value !== undefined ) {
times.push( key.time );
values.push.apply( values, value ); // push all elements
}
key = jsonKeys[ i ++ ];
} while ( key !== undefined );
} else if ( value.toArray !== undefined ) {
// ...assume THREE.Math-ish
do {
value = key[ valuePropertyName ];
if ( value !== undefined ) {
times.push( key.time );
value.toArray( values, values.length );
}
key = jsonKeys[ i ++ ];
} while ( key !== undefined );
} else {
// otherwise push as-is
do {
value = key[ valuePropertyName ];
if ( value !== undefined ) {
times.push( key.time );
values.push( value );
}
key = jsonKeys[ i ++ ];
} while ( key !== undefined );
}
}
};
export { AnimationUtils };
import {
InterpolateLinear,
InterpolateSmooth,
InterpolateDiscrete
} from '../constants.js';
import { CubicInterpolant } from '../math/interpolants/CubicInterpolant.js';
import { LinearInterpolant } from '../math/interpolants/LinearInterpolant.js';
import { DiscreteInterpolant } from '../math/interpolants/DiscreteInterpolant.js';
import { AnimationUtils } from './AnimationUtils.js';
/**
*
* A timed sequence of keyframes for a specific property.
*
*
* @author Ben Houston / http://clara.io/
* @author David Sarno / http://lighthaus.us/
* @author tschw
*/
function KeyframeTrack( name, times, values, interpolation ) {
if ( name === undefined ) throw new Error( 'THREE.KeyframeTrack: track name is undefined' );
if ( times === undefined || times.length === 0 ) throw new Error( 'THREE.KeyframeTrack: no keyframes in track named ' + name );
this.name = name;
this.times = AnimationUtils.convertArray( times, this.TimeBufferType );
this.values = AnimationUtils.convertArray( values, this.ValueBufferType );
this.setInterpolation( interpolation || this.DefaultInterpolation );
}
// Static methods
Object.assign( KeyframeTrack, {
// Serialization (in static context, because of constructor invocation
// and automatic invocation of .toJSON):
toJSON: function ( track ) {
var trackType = track.constructor;
var json;
// derived classes can define a static toJSON method
if ( trackType.toJSON !== undefined ) {
json = trackType.toJSON( track );
} else {
// by default, we assume the data can be serialized as-is
json = {
'name': track.name,
'times': AnimationUtils.convertArray( track.times, Array ),
'values': AnimationUtils.convertArray( track.values, Array )
};
var interpolation = track.getInterpolation();
if ( interpolation !== track.DefaultInterpolation ) {
json.interpolation = interpolation;
}
}
json.type = track.ValueTypeName; // mandatory
return json;
}
} );
Object.assign( KeyframeTrack.prototype, {
constructor: KeyframeTrack,
TimeBufferType: Float32Array,
ValueBufferType: Float32Array,
DefaultInterpolation: InterpolateLinear,
InterpolantFactoryMethodDiscrete: function ( result ) {
return new DiscreteInterpolant( this.times, this.values, this.getValueSize(), result );
},
InterpolantFactoryMethodLinear: function ( result ) {
return new LinearInterpolant( this.times, this.values, this.getValueSize(), result );
},
InterpolantFactoryMethodSmooth: function ( result ) {
return new CubicInterpolant( this.times, this.values, this.getValueSize(), result );
},
setInterpolation: function ( interpolation ) {
var factoryMethod;
switch ( interpolation ) {
case InterpolateDiscrete:
factoryMethod = this.InterpolantFactoryMethodDiscrete;
break;
case InterpolateLinear:
factoryMethod = this.InterpolantFactoryMethodLinear;
break;
case InterpolateSmooth:
factoryMethod = this.InterpolantFactoryMethodSmooth;
break;
}
if ( factoryMethod === undefined ) {
var message = "unsupported interpolation for " +
this.ValueTypeName + " keyframe track named " + this.name;
if ( this.createInterpolant === undefined ) {
// fall back to default, unless the default itself is messed up
if ( interpolation !== this.DefaultInterpolation ) {
this.setInterpolation( this.DefaultInterpolation );
} else {
throw new Error( message ); // fatal, in this case
}
}
console.warn( 'THREE.KeyframeTrack:', message );
return this;
}
this.createInterpolant = factoryMethod;
return this;
},
getInterpolation: function () {
switch ( this.createInterpolant ) {
case this.InterpolantFactoryMethodDiscrete:
return InterpolateDiscrete;
case this.InterpolantFactoryMethodLinear:
return InterpolateLinear;
case this.InterpolantFactoryMethodSmooth:
return InterpolateSmooth;
}
},
getValueSize: function () {
return this.values.length / this.times.length;
},
// move all keyframes either forwards or backwards in time
shift: function ( timeOffset ) {
if ( timeOffset !== 0.0 ) {
var times = this.times;
for ( var i = 0, n = times.length; i !== n; ++ i ) {
times[ i ] += timeOffset;
}
}
return this;
},
// scale all keyframe times by a factor (useful for frame <-> seconds conversions)
scale: function ( timeScale ) {
if ( timeScale !== 1.0 ) {
var times = this.times;
for ( var i = 0, n = times.length; i !== n; ++ i ) {
times[ i ] *= timeScale;
}
}
return this;
},
// removes keyframes before and after animation without changing any values within the range [startTime, endTime].
// IMPORTANT: We do not shift around keys to the start of the track time, because for interpolated keys this will change their values
trim: function ( startTime, endTime ) {
var times = this.times,
nKeys = times.length,
from = 0,
to = nKeys - 1;
while ( from !== nKeys && times[ from ] < startTime ) {
++ from;
}
while ( to !== - 1 && times[ to ] > endTime ) {
-- to;
}
++ to; // inclusive -> exclusive bound
if ( from !== 0 || to !== nKeys ) {
// empty tracks are forbidden, so keep at least one keyframe
if ( from >= to ) to = Math.max( to, 1 ), from = to - 1;
var stride = this.getValueSize();
this.times = AnimationUtils.arraySlice( times, from, to );
this.values = AnimationUtils.arraySlice( this.values, from * stride, to * stride );
}
return this;
},
// ensure we do not get a GarbageInGarbageOut situation, make sure tracks are at least minimally viable
validate: function () {
var valid = true;
var valueSize = this.getValueSize();
if ( valueSize - Math.floor( valueSize ) !== 0 ) {
console.error( 'THREE.KeyframeTrack: Invalid value size in track.', this );
valid = false;
}
var times = this.times,
values = this.values,
nKeys = times.length;
if ( nKeys === 0 ) {
console.error( 'THREE.KeyframeTrack: Track is empty.', this );
valid = false;
}
var prevTime = null;
for ( var i = 0; i !== nKeys; i ++ ) {
var currTime = times[ i ];
if ( typeof currTime === 'number' && isNaN( currTime ) ) {
console.error( 'THREE.KeyframeTrack: Time is not a valid number.', this, i, currTime );
valid = false;
break;
}
if ( prevTime !== null && prevTime > currTime ) {
console.error( 'THREE.KeyframeTrack: Out of order keys.', this, i, currTime, prevTime );
valid = false;
break;
}
prevTime = currTime;
}
if ( values !== undefined ) {
if ( AnimationUtils.isTypedArray( values ) ) {
for ( var i = 0, n = values.length; i !== n; ++ i ) {
var value = values[ i ];
if ( isNaN( value ) ) {
console.error( 'THREE.KeyframeTrack: Value is not a valid number.', this, i, value );
valid = false;
break;
}
}
}
}
return valid;
},
// removes equivalent sequential keys as common in morph target sequences
// (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0)
optimize: function () {
var times = this.times,
values = this.values,
stride = this.getValueSize(),
smoothInterpolation = this.getInterpolation() === InterpolateSmooth,
writeIndex = 1,
lastIndex = times.length - 1;
for ( var i = 1; i < lastIndex; ++ i ) {
var keep = false;
var time = times[ i ];
var timeNext = times[ i + 1 ];
// remove adjacent keyframes scheduled at the same time
if ( time !== timeNext && ( i !== 1 || time !== time[ 0 ] ) ) {
if ( ! smoothInterpolation ) {
// remove unnecessary keyframes same as their neighbors
var offset = i * stride,
offsetP = offset - stride,
offsetN = offset + stride;
for ( var j = 0; j !== stride; ++ j ) {
var value = values[ offset + j ];
if ( value !== values[ offsetP + j ] ||
value !== values[ offsetN + j ] ) {
keep = true;
break;
}
}
} else {
keep = true;
}
}
// in-place compaction
if ( keep ) {
if ( i !== writeIndex ) {
times[ writeIndex ] = times[ i ];
var readOffset = i * stride,
writeOffset = writeIndex * stride;
for ( var j = 0; j !== stride; ++ j ) {
values[ writeOffset + j ] = values[ readOffset + j ];
}
}
++ writeIndex;
}
}
// flush last keyframe (compaction looks ahead)
if ( lastIndex > 0 ) {
times[ writeIndex ] = times[ lastIndex ];
for ( var readOffset = lastIndex * stride, writeOffset = writeIndex * stride, j = 0; j !== stride; ++ j ) {
values[ writeOffset + j ] = values[ readOffset + j ];
}
++ writeIndex;
}
if ( writeIndex !== times.length ) {
this.times = AnimationUtils.arraySlice( times, 0, writeIndex );
this.values = AnimationUtils.arraySlice( values, 0, writeIndex * stride );
}
return this;
}
} );
export { KeyframeTrack };
This diff is collapsed.
import { Quaternion } from '../math/Quaternion.js';
/**
*
* Buffered scene graph property that allows weighted accumulation.
*
*
* @author Ben Houston / http://clara.io/
* @author David Sarno / http://lighthaus.us/
* @author tschw
*/
function PropertyMixer( binding, typeName, valueSize ) {
this.binding = binding;
this.valueSize = valueSize;
var bufferType = Float64Array,
mixFunction;
switch ( typeName ) {
case 'quaternion':
mixFunction = this._slerp;
break;
case 'string':
case 'bool':
bufferType = Array;
mixFunction = this._select;
break;
default:
mixFunction = this._lerp;
}
this.buffer = new bufferType( valueSize * 4 );
// layout: [ incoming | accu0 | accu1 | orig ]
//
// interpolators can use .buffer as their .result
// the data then goes to 'incoming'
//
// 'accu0' and 'accu1' are used frame-interleaved for
// the cumulative result and are compared to detect
// changes
//
// 'orig' stores the original state of the property
this._mixBufferRegion = mixFunction;
this.cumulativeWeight = 0;
this.useCount = 0;
this.referenceCount = 0;
}
Object.assign( PropertyMixer.prototype, {
// accumulate data in the 'incoming' region into 'accu<i>'
accumulate: function ( accuIndex, weight ) {
// note: happily accumulating nothing when weight = 0, the caller knows
// the weight and shouldn't have made the call in the first place
var buffer = this.buffer,
stride = this.valueSize,
offset = accuIndex * stride + stride,
currentWeight = this.cumulativeWeight;
if ( currentWeight === 0 ) {
// accuN := incoming * weight
for ( var i = 0; i !== stride; ++ i ) {
buffer[ offset + i ] = buffer[ i ];
}
currentWeight = weight;
} else {
// accuN := accuN + incoming * weight
currentWeight += weight;
var mix = weight / currentWeight;
this._mixBufferRegion( buffer, offset, 0, mix, stride );
}
this.cumulativeWeight = currentWeight;
},
// apply the state of 'accu<i>' to the binding when accus differ
apply: function ( accuIndex ) {
var stride = this.valueSize,
buffer = this.buffer,
offset = accuIndex * stride + stride,
weight = this.cumulativeWeight,
binding = this.binding;
this.cumulativeWeight = 0;
if ( weight < 1 ) {
// accuN := accuN + original * ( 1 - cumulativeWeight )
var originalValueOffset = stride * 3;
this._mixBufferRegion(
buffer, offset, originalValueOffset, 1 - weight, stride );
}
for ( var i = stride, e = stride + stride; i !== e; ++ i ) {
if ( buffer[ i ] !== buffer[ i + stride ] ) {
// value has changed -> update scene graph
binding.setValue( buffer, offset );
break;
}
}
},
// remember the state of the bound property and copy it to both accus
saveOriginalState: function () {
var binding = this.binding;
var buffer = this.buffer,
stride = this.valueSize,
originalValueOffset = stride * 3;
binding.getValue( buffer, originalValueOffset );
// accu[0..1] := orig -- initially detect changes against the original
for ( var i = stride, e = originalValueOffset; i !== e; ++ i ) {
buffer[ i ] = buffer[ originalValueOffset + ( i % stride ) ];
}
this.cumulativeWeight = 0;
},
// apply the state previously taken via 'saveOriginalState' to the binding
restoreOriginalState: function () {
var originalValueOffset = this.valueSize * 3;
this.binding.setValue( this.buffer, originalValueOffset );
},
// mix functions
_select: function ( buffer, dstOffset, srcOffset, t, stride ) {
if ( t >= 0.5 ) {
for ( var i = 0; i !== stride; ++ i ) {
buffer[ dstOffset + i ] = buffer[ srcOffset + i ];
}
}
},
_slerp: function ( buffer, dstOffset, srcOffset, t ) {
Quaternion.slerpFlat( buffer, dstOffset, buffer, dstOffset, buffer, srcOffset, t );
},
_lerp: function ( buffer, dstOffset, srcOffset, t, stride ) {
var s = 1 - t;
for ( var i = 0; i !== stride; ++ i ) {
var j = dstOffset + i;
buffer[ j ] = buffer[ j ] * s + buffer[ srcOffset + i ] * t;
}
}
} );
export { PropertyMixer };
export var ZeroCurvatureEnding = 2400;
export var ZeroSlopeEnding = 2401;
export var WrapAroundEnding = 2402;
export var LoopOnce = 2200;
export var LoopRepeat = 2201;
export var LoopPingPong = 2202;
export var InterpolateDiscrete = 2300;
export var InterpolateLinear = 2301;
export var InterpolateSmooth = 2302;
\ No newline at end of file
export * from "./AnimationClip3D"
export * from "./AnimationTrack3D"
\ No newline at end of file
import { ZeroCurvatureEnding } from '../constants';
import { Interpolant } from '../../math/Interpolant';
import { WrapAroundEnding, ZeroSlopeEnding } from '../constants';
/**
* Fast and simple cubic spline interpolant.
*
* It was derived from a Hermitian construction setting the first derivative
* at each sample position to the linear slope between neighboring positions
* over their parameter interval.
*
* @author tschw
*/
export class CubicInterpolant extends Interpolant {
_weightPrev: number;
_offsetPrev: number;
_weightNext: number;
_offsetNext: number;
DefaultSettings_ = {
endingStart: ZeroCurvatureEnding,
endingEnd: ZeroCurvatureEnding
}
constructor(parameterPositions, sampleValues, sampleSize, resultBuffer) {
super(parameterPositions, sampleValues, sampleSize, resultBuffer)
this._weightPrev = - 0;
this._offsetPrev = - 0;
this._weightNext = - 0;
this._offsetNext = - 0;
}
intervalChanged_(i1, t0, t1) {
var pp = this.parameterPositions,
iPrev = i1 - 2,
iNext = i1 + 1,
tPrev = pp[iPrev],
tNext = pp[iNext];
if (tPrev === undefined) {
switch (this.getSettings_().endingStart) {
case ZeroSlopeEnding:
// f'(t0) = 0
iPrev = i1;
tPrev = 2 * t0 - t1;
break;
case WrapAroundEnding:
// use the other end of the curve
iPrev = pp.length - 2;
tPrev = t0 + pp[iPrev] - pp[iPrev + 1];
break;
default: // ZeroCurvatureEnding
// f''(t0) = 0 a.k.a. Natural Spline
iPrev = i1;
tPrev = t1;
}
}
if (tNext === undefined) {
switch (this.getSettings_().endingEnd) {
case ZeroSlopeEnding:
// f'(tN) = 0
iNext = i1;
tNext = 2 * t1 - t0;
break;
case WrapAroundEnding:
// use the other end of the curve
iNext = 1;
tNext = t1 + pp[1] - pp[0];
break;
default: // ZeroCurvatureEnding
// f''(tN) = 0, a.k.a. Natural Spline
iNext = i1 - 1;
tNext = t0;
}
}
var halfDt = (t1 - t0) * 0.5,
stride = this.valueSize;
this._weightPrev = halfDt / (t0 - tPrev);
this._weightNext = halfDt / (tNext - t1);
this._offsetPrev = iPrev * stride;
this._offsetNext = iNext * stride;
};
interpolate_(i1, t0, t, t1) {
var result = this.resultBuffer,
values = this.sampleValues,
stride = this.valueSize,
o1 = i1 * stride, o0 = o1 - stride,
oP = this._offsetPrev, oN = this._offsetNext,
wP = this._weightPrev, wN = this._weightNext,
p = (t - t0) / (t1 - t0),
pp = p * p,
ppp = pp * p;
// evaluate polynomials
var sP = - wP * ppp + 2 * wP * pp - wP * p;
var s0 = (1 + wP) * ppp + (- 1.5 - 2 * wP) * pp + (- 0.5 + wP) * p + 1;
var s1 = (- 1 - wN) * ppp + (1.5 + wN) * pp + 0.5 * p;
var sN = wN * ppp - wN * pp;
// combine data linearly
for (var i = 0; i !== stride; ++i) {
result[i] =
sP * values[oP + i] +
s0 * values[o0 + i] +
s1 * values[o1 + i] +
sN * values[oN + i];
}
return result;
}
}
import { Interpolant } from '../../math/Interpolant';
/**
*
* Interpolant that evaluates to the sample value at the position preceeding
* the parameter.
*
* @author tschw
*/
export class DiscreteInterpolant extends Interpolant {
constructor(parameterPositions, sampleValues, sampleSize, resultBuffer) {
super(parameterPositions, sampleValues, sampleSize, resultBuffer);
}
interpolate_(i1 /*, t0, t, t1 */) {
return this.copySampleValue_(i1 - 1);
}
}
This diff is collapsed.
import { Interpolant } from '../Interpolant.js';
import { Quaternion } from '../Quaternion.js';
/**
* Spherical linear unit quaternion interpolant.
*
* @author tschw
*/
function QuaternionLinearInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) {
Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer );
}
QuaternionLinearInterpolant.prototype = Object.assign( Object.create( Interpolant.prototype ), {
constructor: QuaternionLinearInterpolant,
interpolate_: function ( i1, t0, t, t1 ) {
var result = this.resultBuffer,
values = this.sampleValues,
stride = this.valueSize,
offset = i1 * stride,
alpha = ( t - t0 ) / ( t1 - t0 );
for ( var end = offset + stride; offset !== end; offset += 4 ) {
Quaternion.slerpFlat( result, 0, values, offset - stride, values, offset, alpha );
}
return result;
}
} );
export { QuaternionLinearInterpolant };
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment