Compare commits

..

10 Commits

Author SHA1 Message Date
Alexander Rose
24b36f41da 2.0.0-dev.1 2021-02-15 22:09:05 -08:00
Alexander Rose
c9c890782c try re-use boundingSphere in element visuals
- if it has not changed much
2021-02-15 21:38:13 -08:00
Alexander Rose
f2c539ebd8 psf parser, support lammps "full" style 2021-02-15 18:04:38 -08:00
dsehnal
feb922ca91 Merge branch 'gpu' 2021-02-14 20:13:16 +01:00
dsehnal
25127bb84b Merge branch 'master' of https://github.com/molstar/molstar 2021-02-14 20:13:12 +01:00
dsehnal
8fb01d2157 Merge remote-tracking branch 'origin' into gpu 2021-02-14 20:11:11 +01:00
dsehnal
c09357ea75 updateImmediate for modelIndex 2021-02-14 20:00:37 +01:00
dsehnal
9f2513dae0 fix examples 2021-02-14 19:38:58 +01:00
dsehnal
11a52c0390 add missing TrajectoryInfo 2021-02-14 19:34:32 +01:00
dsehnal
e955dc7e94 exportable trajectory animation 2021-02-14 19:26:06 +01:00
12 changed files with 200 additions and 72 deletions

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "2.0.0-dev.0",
"version": "2.0.0-dev.1",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "2.0.0-dev.0",
"version": "2.0.0-dev.1",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {

View File

@@ -86,7 +86,7 @@
// adjust this number to make the animation faster or slower
// requires to "restart" the animation if changed
BasicMolStarWrapper.animate.modelIndex.maxFPS = 30;
BasicMolStarWrapper.animate.modelIndex.targetFps = 30;
addControl('Play To End', () => BasicMolStarWrapper.animate.modelIndex.onceForward());
addControl('Play To Start', () => BasicMolStarWrapper.animate.modelIndex.onceBackward());

View File

@@ -83,13 +83,17 @@ class BasicWrapper {
if (!this.plugin.canvas3d.props.trackball.spin) PluginCommands.Camera.Reset(this.plugin, {});
}
private animateModelIndexTargetFps() {
return Math.max(1, this.animate.modelIndex.targetFps | 0);
}
animate = {
modelIndex: {
maxFPS: 8,
onceForward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }); },
onceBackward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }); },
palindrome: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }); },
loop: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }); },
targetFps: 8,
onceForward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'once', params: { direction: 'forward' } } }); },
onceBackward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'once', params: { direction: 'backward' } } }); },
palindrome: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'palindrome', params: {} } }); },
loop: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'loop', params: {} } }); },
stop: () => this.plugin.managers.animation.stop()
}
}

View File

@@ -147,7 +147,7 @@
// adjust this number to make the animation faster or slower
// requires to "restart" the animation if changed
PluginWrapper.animate.modelIndex.maxFPS = 30;
PluginWrapper.animate.modelIndex.targetFps = 30;
addControl('Play To End', () => PluginWrapper.animate.modelIndex.onceForward());
addControl('Play To Start', () => PluginWrapper.animate.modelIndex.onceBackward());

View File

@@ -272,13 +272,17 @@ class MolStarProteopediaWrapper {
resetPosition: () => PluginCommands.Camera.Reset(this.plugin, { })
}
private animateModelIndexTargetFps() {
return Math.max(1, this.animate.modelIndex.targetFps | 0);
}
animate = {
modelIndex: {
maxFPS: 8,
onceForward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }); },
onceBackward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }); },
palindrome: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }); },
loop: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }); },
targetFps: 8,
onceForward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'once', params: { direction: 'forward' } } }); },
onceBackward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'once', params: { direction: 'backward' } } }); },
palindrome: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'palindrome', params: {} } }); },
loop: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'loop', params: {} } }); },
stop: () => this.plugin.managers.animation.stop()
}
}

View File

@@ -58,24 +58,44 @@ async function handleAtoms(state: State, count: number): Promise<PsfFile['atoms'
const charge = TokenBuilder.create(tokenizer.data, count * 2);
const mass = TokenBuilder.create(tokenizer.data, count * 2);
const { position } = tokenizer;
const line = readLine(tokenizer).trim();
tokenizer.position = position;
// LAMMPS full
// AtomID ResID AtomName AtomType Charge Mass Unused0
const isLammpsFull = line.split(reWhitespace).length === 7;
const n = isLammpsFull ? 6 : 8;
const { length } = tokenizer;
let linesAlreadyRead = 0;
await chunkedSubtask(state.runtimeCtx, 10, void 0, chunkSize => {
await chunkedSubtask(state.runtimeCtx, 100000, void 0, chunkSize => {
const linesToRead = Math.min(count - linesAlreadyRead, chunkSize);
for (let i = 0; i < linesToRead; ++i) {
for (let j = 0; j < 8; ++j) {
for (let j = 0; j < n; ++j) {
skipWhitespace(tokenizer);
markStart(tokenizer);
eatValue(tokenizer);
switch (j) {
case 0: TokenBuilder.addUnchecked(atomId, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 1: TokenBuilder.addUnchecked(segmentName, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 2: TokenBuilder.addUnchecked(residueId, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 3: TokenBuilder.addUnchecked(residueName, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 4: TokenBuilder.addUnchecked(atomName, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 5: TokenBuilder.addUnchecked(atomType, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 6: TokenBuilder.addUnchecked(charge, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 7: TokenBuilder.addUnchecked(mass, tokenizer.tokenStart, tokenizer.tokenEnd); break;
if (isLammpsFull) {
switch (j) {
case 0: TokenBuilder.addUnchecked(atomId, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 1: TokenBuilder.addUnchecked(residueId, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 2: TokenBuilder.addUnchecked(atomName, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 3: TokenBuilder.addUnchecked(atomType, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 4: TokenBuilder.addUnchecked(charge, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 5: TokenBuilder.addUnchecked(mass, tokenizer.tokenStart, tokenizer.tokenEnd); break;
}
} else {
switch (j) {
case 0: TokenBuilder.addUnchecked(atomId, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 1: TokenBuilder.addUnchecked(segmentName, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 2: TokenBuilder.addUnchecked(residueId, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 3: TokenBuilder.addUnchecked(residueName, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 4: TokenBuilder.addUnchecked(atomName, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 5: TokenBuilder.addUnchecked(atomType, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 6: TokenBuilder.addUnchecked(charge, tokenizer.tokenStart, tokenizer.tokenEnd); break;
case 7: TokenBuilder.addUnchecked(mass, tokenizer.tokenStart, tokenizer.tokenEnd); break;
}
}
}
// ignore any extra columns
@@ -89,9 +109,13 @@ async function handleAtoms(state: State, count: number): Promise<PsfFile['atoms'
return {
count,
atomId: TokenColumn(atomId)(Column.Schema.int),
segmentName: TokenColumn(segmentName)(Column.Schema.str),
segmentName: isLammpsFull
? TokenColumn(residueId)(Column.Schema.str)
: TokenColumn(segmentName)(Column.Schema.str),
residueId: TokenColumn(residueId)(Column.Schema.int),
residueName: TokenColumn(residueName)(Column.Schema.str),
residueName: isLammpsFull
? TokenColumn(residueId)(Column.Schema.str)
: TokenColumn(residueName)(Column.Schema.str),
atomName: TokenColumn(atomName)(Column.Schema.str),
atomType: TokenColumn(atomType)(Column.Schema.str),
charge: TokenColumn(charge)(Column.Schema.float),

View File

@@ -119,6 +119,8 @@ export namespace Model {
ModelSymmetry.Provider.set(m, symmetry);
}
TrajectoryInfo.set(m, { index: i, size: frames.length });
trajectory.push(m);
}
return { trajectory, srcIndexArray };

View File

@@ -14,13 +14,24 @@ import { PluginStateAnimation } from '../model';
export const AnimateModelIndex = PluginStateAnimation.create({
name: 'built-in.animate-model-index',
display: { name: 'Animate Trajectory' },
isExportable: true,
params: () => ({
mode: PD.MappedStatic('palindrome', {
mode: PD.MappedStatic('loop', {
palindrome: PD.Group({ }),
loop: PD.Group({ }),
once: PD.Group({ direction: PD.Select('forward', [['forward', 'Forward'], ['backward', 'Backward']]) }, { isFlat: true })
}, { options: [['palindrome', 'Palindrome'], ['loop', 'Loop'], ['once', 'Once']] }),
maxFPS: PD.Numeric(15, { min: 1, max: 60, step: 1 })
duration: PD.MappedStatic('fixed', {
fixed: PD.Group({
durationInS: PD.Numeric(5, { min: 1, max: 120, step: 0.1 }, { description: 'Duration in seconds' })
}, { isFlat: true }),
computed: PD.Group({
targetFps: PD.Numeric(30, { min: 5, max: 250, step: 1 }, { label: 'Target FPS' })
}, { isFlat: true }),
sequential: PD.Group({
maxFps: PD.Numeric(30, { min: 5, max: 60, step: 1 })
}, { isFlat: true })
})
}),
canApply(ctx) {
const state = ctx.state.data;
@@ -31,10 +42,30 @@ export const AnimateModelIndex = PluginStateAnimation.create({
}
return { canApply: false, reason: 'No trajectory to animate' };
},
getDuration: (p, ctx) => {
if (p.duration?.name === 'fixed') {
return { kind: 'fixed', durationMs: p.duration.params.durationInS * 1000 };
} else if (p.duration.name === 'computed') {
const state = ctx.state.data;
const models = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Model.ModelFromTrajectory));
let maxDuration = 0;
for (const m of models) {
const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, PluginStateObject.Molecule.Trajectory);
if (!parent || !parent.obj) continue;
const traj = parent.obj;
maxDuration = Math.max(Math.ceil(1000 * traj.data.frameCount / p.duration.params.targetFps), maxDuration);
}
return { kind: 'fixed', durationMs: maxDuration };
}
return { kind: 'unknown' };
},
initialState: () => ({} as { palindromeDirections?: { [id: string]: -1 | 1 | undefined } }),
async apply(animState, t, ctx) {
// limit fps
if (t.current > 0 && t.current - t.lastApplied < 1000 / ctx.params.maxFPS) {
if (ctx.params.duration.name === 'sequential' && t.current > 0 && t.current - t.lastApplied < 1000 / ctx.params.duration.params.maxFps) {
return { kind: 'skip' };
}
@@ -65,27 +96,45 @@ export const AnimateModelIndex = PluginStateAnimation.create({
} else {
return old;
}
let dir: -1 | 1 = 1;
if (params.mode.name === 'once') {
dir = params.mode.params.direction === 'backward' ? -1 : 1;
// if we are at start or end already, do nothing.
if ((dir === -1 && old.modelIndex === 0) || (dir === 1 && old.modelIndex === len - 1)) {
isEnd = true;
return old;
if (params.duration.name === 'sequential') {
let dir: -1 | 1 = 1;
if (params.mode.name === 'once') {
dir = params.mode.params.direction === 'backward' ? -1 : 1;
// if we are at start or end already, do nothing.
if ((dir === -1 && old.modelIndex === 0) || (dir === 1 && old.modelIndex === len - 1)) {
isEnd = true;
return old;
}
} else if (params.mode.name === 'palindrome') {
if (old.modelIndex === 0) dir = 1;
else if (old.modelIndex === len - 1) dir = -1;
else dir = palindromeDirections[m.transform.ref] || 1;
}
} else if (params.mode.name === 'palindrome') {
if (old.modelIndex === 0) dir = 1;
else if (old.modelIndex === len - 1) dir = -1;
else dir = palindromeDirections[m.transform.ref] || 1;
palindromeDirections[m.transform.ref] = dir;
let modelIndex = (old.modelIndex + dir) % len;
if (modelIndex < 0) modelIndex += len;
isEnd = isEnd || (dir === -1 && modelIndex === 0) || (dir === 1 && modelIndex === len - 1);
return { modelIndex };
} else {
const durationInMs = params.duration.name === 'fixed'
? params.duration.params.durationInS * 1000
: Math.ceil(1000 * traj.data.frameCount / params.duration.params.targetFps);
let phase: number = (t.current % durationInMs) / durationInMs;
if (params.mode.name === 'palindrome') {
phase = 2 * phase;
if (phase > 1) phase = 2 - phase;
}
const modelIndex = Math.min(Math.floor(traj.data.frameCount * phase), traj.data.frameCount - 1);
isEnd = isEnd || modelIndex === traj.data.frameCount - 1;
return { modelIndex };
}
palindromeDirections[m.transform.ref] = dir;
let modelIndex = (old.modelIndex + dir) % len;
if (modelIndex < 0) modelIndex += len;
isEnd = isEnd || (dir === -1 && modelIndex === 0) || (dir === 1 && modelIndex === len - 1);
return { modelIndex };
});
}

View File

@@ -345,9 +345,9 @@ const ModelFromTrajectory = PluginStateTransform.BuiltIn({
to: SO.Molecule.Model,
params: a => {
if (!a) {
return { modelIndex: PD.Numeric(0, {}, { description: 'Zero-based index of the model' }) };
return { modelIndex: PD.Numeric(0, {}, { description: 'Zero-based index of the model', immediateUpdate: true }) };
}
return { modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.frameCount, step: 1 }, { description: 'Model Index' })) };
return { modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.frameCount, step: 1 }, { description: 'Model Index', immediateUpdate: true })) };
}
})({
isApplicable: a => a.data.frameCount > 0,

View File

@@ -1,23 +1,23 @@
/**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as React from 'react';
import { StructureHierarchyRef, ModelRef, TrajectoryRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
import { Model } from '../../mol-model/structure';
import { ModelRef, StructureHierarchyRef, TrajectoryRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { StateSelection } from '../../mol-state';
import { CollapsableControls, CollapsableState } from '../base';
import { ActionMenu } from '../controls/action-menu';
import { Button, IconButton, ExpandGroup } from '../controls/common';
import { Button, ExpandGroup, IconButton } from '../controls/common';
import { BookmarksOutlinedSvg, MoleculeSvg } from '../controls/icons';
import { ParameterControls } from '../controls/parameters';
import { StructureFocusControls } from './focus';
import { UpdateTransformControl } from '../state/update-transform';
import { StructureFocusControls } from './focus';
import { StructureSelectionStatsControls } from './selection';
import { StateSelection } from '../../mol-state';
import { MoleculeSvg, BookmarksOutlinedSvg } from '../controls/icons';
import { Model } from '../../mol-model/structure';
interface StructureSourceControlState extends CollapsableState {
isBusy: boolean,
@@ -214,13 +214,30 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
mng.hierarchy.applyPreset(trajectories, item.value as any);
}
updateStructureModel = async (params: any) => {
const { selection } = this.plugin.managers.structure.hierarchy;
const m = selection.structures[0].model!;
this.plugin.state.updateTransform(this.plugin.state.data, m.cell.transform.ref, params, 'Model Index');
// TODO: ?? PluginCommands.Camera.Reset(this.plugin);
private updateModelQueueParams: any = void 0;
private isUpdatingModel = false;
private async _updateStructureModel() {
if (!this.updateModelQueueParams || this.isUpdatingModel) return;
const params = this.updateModelQueueParams;
this.updateModelQueueParams = void 0;
try {
this.isUpdatingModel = true;
const { selection } = this.plugin.managers.structure.hierarchy;
const m = selection.structures[0].model!;
await this.plugin.state.updateTransform(this.plugin.state.data, m.cell.transform.ref, params, 'Model Index');
} finally {
this.isUpdatingModel = false;
this._updateStructureModel();
}
}
updateStructureModel = (params: any) => {
this.updateModelQueueParams = params;
this._updateStructureModel();
};
get modelIndex() {
const { selection } = this.plugin.managers.structure.hierarchy;
if (selection.structures.length !== 1) return null;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -23,6 +23,9 @@ import { SpheresBuilder } from '../../../../mol-geo/geometry/spheres/spheres-bui
import { isTrace, isH } from './common';
import { Sphere3D } from '../../../../mol-math/geometry';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3add = Vec3.add;
type ElementProps = {
ignoreHydrogens: boolean,
traceOnly: boolean,
@@ -59,27 +62,38 @@ export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structur
const v = Vec3();
const pos = unit.conformation.invariantPosition;
const ignore = makeElementIgnoreTest(unit, props);
const l = StructureElement.Location.create(structure);
l.unit = unit;
const l = StructureElement.Location.create(structure, unit);
const themeSize = theme.size.size;
const center = Vec3();
let maxSize = 0;
let count = 0;
for (let i = 0; i < elementCount; i++) {
if (ignore && ignore(unit, elements[i])) continue;
l.element = elements[i];
pos(elements[i], v);
v3add(center, center, v);
count += 1;
builderState.currentGroup = i;
const size = theme.size.size(l);
const size = themeSize(l);
if (size > maxSize) maxSize = size;
addSphere(builderState, v, size * sizeFactor, detail);
}
// re-use boundingSphere if it has not changed much
let boundingSphere: Sphere3D;
Vec3.scale(center, center, 1 / count);
if (mesh && Vec3.distance(center, mesh.boundingSphere.center) / mesh.boundingSphere.radius < 1.0) {
boundingSphere = Sphere3D.clone(mesh.boundingSphere);
} else {
boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, maxSize * sizeFactor + 0.05);
}
const m = MeshBuilder.getMesh(builderState);
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, maxSize * sizeFactor + 0.05);
m.setBoundingSphere(sphere);
m.setBoundingSphere(boundingSphere);
return m;
}
@@ -98,21 +112,35 @@ export function createElementSphereImpostor(ctx: VisualContext, unit: Unit, stru
const ignore = makeElementIgnoreTest(unit, props);
const l = StructureElement.Location.create(structure, unit);
const themeSize = theme.size.size;
const center = Vec3();
let maxSize = 0;
let count = 0;
for (let i = 0; i < elementCount; i++) {
if (ignore?.(unit, elements[i])) continue;
pos(elements[i], v);
builder.add(v[0], v[1], v[2], i);
v3add(center, center, v);
count += 1;
l.element = elements[i];
const size = theme.size.size(l);
const size = themeSize(l);
if (size > maxSize) maxSize = size;
}
// re-use boundingSphere if it has not changed much
let boundingSphere: Sphere3D;
Vec3.scale(center, center, 1 / count);
if (spheres && Vec3.distance(center, spheres.boundingSphere.center) / spheres.boundingSphere.radius < 1.0) {
boundingSphere = Sphere3D.clone(spheres.boundingSphere);
} else {
boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, maxSize * props.sizeFactor + 0.05);
}
const s = builder.getSpheres();
s.setBoundingSphere(Sphere3D.expand(Sphere3D(), unit.boundary.sphere, maxSize * props.sizeFactor + 0.05));
s.setBoundingSphere(boundingSphere);
return s;
}