Java-Gaming.org Hi !
Featured games (91)
games approved by the League of Dukes
Games in Showcase (804)
Games in Android Showcase (239)
games submitted by our members
Games in WIP (868)
games currently in development
News: Read the Java Gaming Resources, or peek at the official Java tutorials
 
    Home     Help   Search   Login   Register   
Pages: 1 2 3 [4] 5 6 ... 13
  ignore  |  Print  
  Java OpenGL Math Library (JOML)  (Read 215913 times)
0 Members and 1 Guest are viewing this topic.
Offline theagentd
« Reply #90 - Posted 2015-07-10 20:09:44 »

An empty Culler() constructor would be nice, as they're usually created ahead of time and reused for each frame.

Using Culler:

Point culling: ~1.5% faster
Sphere culling: ~1% faster
AABB culling: ~33% faster

Impressive AABB results!

Myomyomyo.
Offline theagentd
« Reply #91 - Posted 2015-07-10 20:18:12 »

A proper frustum culler really needs to have distance based culling though. The tests so far has been with distance culling disabled in my own culler. With it enabled my code blows it out of the water:

TestMy culler% visibleJOML% visible
Point culling169 329k0.7891%67 132k1.2311%
Sphere culling178 325k1.0543%68 786k1.6708%
AABB culling43 084k0.9672%53 637k1.4816%

With distance based culling, you get rid of a lot more stuff, in addition to improving performance a lot, at least for sphere and point rendering. AABB culling actually suffers a bit, but the improved accuracy, culling 1/3rd of the remaining AABBs is well worth it IMO.

EDIT: Part of this gain lies in the fact that it culls some things that are inside the frustum but at least my engine are completely covered with fog (it's based on distance from camera, not the z-buffer), and it also only works for perspective matrices.

Myomyomyo.
Offline KaiHH

JGO Kernel


Medals: 794



« Reply #92 - Posted 2015-07-10 20:23:43 »

Could you please clarify more about which distance (from where to where) you mean?
But, I wanted to have a simple working solution in JOML, I'll leave the most performant solution to you. Cheesy
If you like, you can also contribute. I'd be glad to accept any Pull Requests.
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline theagentd
« Reply #93 - Posted 2015-07-10 20:33:54 »

If you look in the culler code I posted, I do a simple squared distance-from-the-camera test for points and similar tests for spheres and AABBs before testing it against any planes. For most game levels the world is significantly larger than the view frustum, so this culls a large number of objects.

For spheres for example:
1  
2  
3  
4  
5  
6  
7  
      float dx = x - this.x, dy = y - this.y, dz = z - this.z;
      float distSqrd = dx*dx + dy*dy + dz*dz;
     
      float r = viewDistance + radius;
      if(distSqrd > r*r){
         return false; //Cannot be visible
      }


You may also want to order the frustums in a way that culls as much as possible as early as possible. A distance-from-the-camera test pretty much culls the same things as the far plane (a little bit more as it curves inwards around the edges, culling things that are technically inside the frustum but covered in fog), but it doesn't cull much behind the camera. The optimal order therefore becomes something like this:

1. Distance test. Only objects in a sphere around the camera passes.
2. Near plane. Cuts out the half of the sphere that's behind the camera.
3. Left and right planes. Those planes generally cull more than the top and bottom planes as most games have horizontal maps.
4. Top and bottom planes. These usually don't cull much.
5. OPTIONAL: The far plane. The initial sphere test culls everything beyond the far-plane anyway.

Also, an extremely useful function is being able to pass in a variable viewDistance when culling. In my culler, I actually cull against min(farPlane, maxDistance), where farPlane is the far plane distance of the camera and maxDistance is a parameter that you can pass into  overloaded versions of the culling methods. For example, I have objects that are only rendered up to a certain distance. Checking if they're inside the frustum and THEN checking if they're within a certain range is rather stupid as it checks the distance twice. Having it as a feature in the frustum culler is definitely a good idea IMO.

Myomyomyo.
Offline KaiHH

JGO Kernel


Medals: 794



« Reply #94 - Posted 2015-07-10 20:41:12 »

Nice ideas!
Yeah, it would be possible to compute the circumscribed sphere of the viewing frustum (we know the frustum's eight corners easily) and then do a simple radius/distance check with points and spheres. I will add that.
But for AABBs what would you test the viewDistance with? Would it make sense to also compute the circumscribed sphere of the AABB, too, and then compare sphere vs. sphere?
Offline theagentd
« Reply #95 - Posted 2015-07-10 20:50:50 »

Nice ideas!
Yeah, it would be possible to compute the circumscribed sphere of the viewing frustum (we know the frustum's eight corners easily) and then do a simple radius/distance check with points and spheres. I will add that.
But for AABBs what would you test the viewDistance with? Would it make sense to also compute the circumscribed sphere of the AABB, too, and then compare sphere vs. sphere?
For the sphere to cull as much as possible, you want the sphere to be centered at the camera's position, e.g. the origin of the perspective matrix. That makes the sphere cull along the edge of the fog, getting rid of everything that would've been invisible anyway. For orthographic projections, then the case is a bit different.

For AABBs, you can either compute a bounding sphere or calculate the exact distance to it. http://stackoverflow.com/questions/5254838/calculating-distance-between-a-point-and-a-rectangular-box-nearest-point

Myomyomyo.
Offline KaiHH

JGO Kernel


Medals: 794



« Reply #96 - Posted 2015-07-11 13:39:12 »

Further suggestions:
 - ...
 - I see you made an multiply-and-add function (fma())! Awesome! It only takes in two vectors though, so one that takes in a float for the multiplier would be nice, fma(Vector3, float).
 - The arguments of fma() could be given better names. They're currently (v1, v2), which says nothing about what they do. May I suggest "add" and "multiplier" for example?
 - Scalar version of Vector*.add() and sub() would be nice too. I have at least one place in my code that does that.
 - Many functions in quaternion also implicitly normalizes the quaternion. The weirdest one is invert(), but many others do too. I believe the user should be in charge of normalizing quaternions and providing normalized inputs to functions that need it.

Questions:
 - A number of quaternion functions seem to internally use doubles. Is there a reasoning behind that?
Sorry, theagentd, I realized I did not respond to your questions and suggestions.
About the Vector*.fma() with scalar: It's in now.
About the arguments of Vector*.fma(): The are both multiplicands. The result (as stated by the JavaDocs) is: this.fma(a, b) = this + a * b
About scalar versions of Vector*.add/sub: They were already provided when you proposed it.
About quaternion normalization: I added another Quaternion.unitInvert() - named after Ogre's identical function - which assumes that the quaternion is already normalized and then just computes its conjugate. Apart from this function, I do not see any other function that implicitly normalizes the quaternion, apart from the various functions to build a quaternion from axis-angle or other different rotation representations, which require the quaternion to be normalized in order to represent only a rotation and not also a scaling.
About some of the quaternion functions using doubles: That's just for additional precision, which is indeed improved when using double for intermediate calculations.
Offline Roquen

JGO Kernel


Medals: 518



« Reply #97 - Posted 2015-07-11 16:14:41 »

I'm on vacation for the next few weeks.  I took a quick skimmed and could give a very large number of feedback points.  Some quick ones.  Generally nobody really cares about 4x4 matrices...what they really want is 4x3.  There's tons of examples of reducible operations, sometime at the cost of adding a rounding step...who cares.  The quaternion function unitInvert should indicate it's proper name: conjugate.

The slerp implementation is a classic example of a function one always sees that should never, ever be called.
Offline KaiHH

JGO Kernel


Medals: 794



« Reply #98 - Posted 2015-07-11 16:54:29 »

Thanks for taking the time to assess and evaluate JOML!
Much appreciated!
I am always happy to hear about any constructive criticism and suggestions you have to improve JOML on the parts you see don't fit your requirements. Again, anyone is free to use the Issues section on GitHub for change or feature requests.
As for the 4x4 matrices, please understand that JOML wants to support the full range of possible transformations, especially orthographic and perspective projections, which need that last matrix row.
There is however the Matrix4.mul4x3() method which assumes that its right operand has (0, 0, 0, 1) as its last row.
Oh, and happy vacations! Smiley
Offline gouessej
« Reply #99 - Posted 2015-07-11 22:53:07 »

theagentd, where is your source code? You claim that your own frustum culling is faster but how can I check that?

Is there a benchmark that I can run on my machine?

Julien Gouesse | Personal blog | Website | Jogamp
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline theagentd
« Reply #100 - Posted 2015-07-12 00:23:07 »

I posted it above.

http://www.java-gaming.org/?action=pastebin&id=1307

It's only faster when the distance test is used first, and when the level is significantly bigger than the frustum so the distance test actually culls anything.

Myomyomyo.
Offline theagentd
« Reply #101 - Posted 2015-07-12 05:26:37 »

Quaternion functions that implicitly normalizes something:
 - get(AxisAngle4f). Does an invalid (?) check and renormalizes the quaternion.
 - rotationAxis(float angle, float axisX, float axisY, float axisZ) implicitly normalizes the input axis (not the quaternion).
 - invert(). I don't think having a separate unitInvert() in there is good. It's the programmers responsibility to give good input.
 - div(Quaternionf b, Quaternionf dest) normalizes the input quaternion b.
 - lookRotate(float dirX, float dirY, float dirZ, float upX, float upY, float upZ, Quaternionf dest) normalizes a lot of stuff weirdly. It normalises the dirXYZ vector but not the upXYZ vector. It shouldn't normalize either IMO.
 - rotationTo(float fromDirX, float fromDirY, float fromDirZ, float toDirX, float toDirY, float toDirZ) normalizes the input stuff.
 - rotateTo(float fromDirX, float fromDirY, float fromDirZ, float toDirX, float toDirY, float toDirZ, Quaternionf dest) as above.
 - rotateAxis(float angle, float axisX, float axisY, float axisZ, Quaternionf dest) normalizes the axis.
 - difference(Quaternionf other, Quaternionf dest) normalizes the other quaternion but not itself.

I completely think that normalizing should be the user's responsibility. In practice all functions expect normalized inputs, so it's hardly difficult to for the user to keep track of. Those extra unnecessary normalizations do cost some performance after all.

The slerp implementation is a classic example of a function one always sees that should never, ever be called.
Why? Do you like having uninterpolated or distorted bone animation?

Myomyomyo.
Offline Roquen

JGO Kernel


Medals: 518



« Reply #102 - Posted 2015-07-12 13:04:03 »

By no implicit normalization I only referring to operations on quaternions.  Conversions in & out need to do the correct thing.  'unitInvert' just needs to be properly named, but the conjugate is an axiomatic function.

The difference function is another poorly named function and no normaliztion should be performed.  Of more general interest would be cmul:  A*B and mulc AB*, which covers and is much more efficient that this.

There are a large number of useful functions I could suggest. Off the top of my head:

Cayley (stereographic) & inverse projections
unit log
pure exp
orientation change:  get Q that rotates unit vector A into unit vector B (this reduces further if actually a matrix is desired)
unit square root
unit square

Having a function called div is IMO awkward since there's right and left versions, but since most people only care about unit quaterions I probably wouldn't include either.  Those that do care will most of the time be able to carry through the derivation of the total desired operation.

Remember that there's absolutely nothing special about unit quaternions. Non-unit quaternions have uses.

On slerp:  Implementations like this have zero use cases.  I'm too lazy to have this conversation from my cell phone though.  Basically slerp is just linearly parameterizing an arc length (2D) and the angle is no more than PI/2 for random input.  It's always reducible to lerp.  The only question is the method of reduction.  If you want a quick answer try twitting @rygorous
Offline gouessej
« Reply #103 - Posted 2015-07-12 16:29:20 »

Remember that there's absolutely nothing special about unit quaternions. Non-unit quaternions have uses.
Ok but only the unit quaternions represent rotations.

Julien Gouesse | Personal blog | Website | Jogamp
Offline Roquen

JGO Kernel


Medals: 518



« Reply #104 - Posted 2015-07-13 00:10:01 »

Not true.  All quaternions other than zero represent a rotation.
Offline theagentd
« Reply #105 - Posted 2015-07-13 07:31:50 »

We've decided to make the move to JOML for Insomnia after the release of Demo 7.

Myomyomyo.
Offline KaiHH

JGO Kernel


Medals: 794



« Reply #106 - Posted 2015-07-13 08:03:06 »

We've decided to make the move to JOML for Insomnia after the release of Demo 7.
Glad to hear that! If there is anything we can do to make JOML easier or faster for you, please let us know!
Additionally, I am investigating a possible performance improvement for JOML using native SSE code. See this enhancement issue: https://github.com/JOML-CI/JOML/issues/30
I'd be happy to hear about any suggestions you have on this.
By the way: org.joml:joml:1.4.0 and org.joml:joml-mini:1.4.0 are now on Maven Central (the first release on Maven Central actually).
Intermediary snapshot releases will still be available on Sonatype's snapshot repository:
https://oss.sonatype.org/content/repositories/snapshots and the next joml release on Central will likely be 1.5.0 in two weeks.
Offline Roquen

JGO Kernel


Medals: 518



« Reply #107 - Posted 2015-07-13 08:30:37 »

There are much bigger gains to be had than sse atm.  Notably lowering memory stalls.  These two go hand in hand.  The latter needs to be addressed before the former has what it needs.  And as mentioned previously there's quite a bit of reworking that can be performed.
Offline KaiHH

JGO Kernel


Medals: 794



« Reply #108 - Posted 2015-07-13 08:52:45 »

@Roquen, I really like your comments. Smiley
Btw. aren't you supposed to be on vacations by now? So why are you on your laptop/mobile?
Enjoy the beach/sun/mountains/culture or wherever it is you are... Cheesy
Now, I like your comments because they fall in line with either of the following:
a) this is bad
b) that should not be done this way
c) people would never want to use this
Everytime I read one of your comments, I can categorize them in either a), b) or c). Smiley
That makes it really easy for me, thanks!
But seriously now: I would happily implement any suggestion you have, if they:
- a) come in a clearly stated form which propose *how* something can be changed for the better
- b) would be useful to YOU to help you with YOUR projects
The point b) will be the motivation for me to actually do it, and a) would tell me what it is that must be done.
So, if you can outline more where you see memory stalls and what we can do about it, I would be happy to fix that.
Again, thanks for your time and thoughts.
Offline gouessej
« Reply #109 - Posted 2015-07-13 12:04:10 »

Not true.  All quaternions other than zero represent a rotation.
Reference (page 4):
http://www.cs.ucr.edu/~vbz/resources/quatut.pdf
http://people.csail.mit.edu/bkph/articles/Quaternions.pdf
Quote
If N(q)  = 1, then q = [vˆ sin W, cos W] acts to rotate around unit axis vˆ by 2W
Quote
we can henceforth assume q is a unit quaternion
Quote
it has to be a unit quaternion

Edit.: Sorry for the off topic.

Julien Gouesse | Personal blog | Website | Jogamp
Offline Roquen

JGO Kernel


Medals: 518



« Reply #110 - Posted 2015-07-13 16:00:27 »

Split quaternion topic non specific.  I am on vacation cell only so more terse than usual.

 I can back up all my commentary..ask for specifics if my reasoning isn't clear.
Offline KaiHH

JGO Kernel


Medals: 794



« Reply #111 - Posted 2015-07-14 09:41:24 »

Just one info for high-performance people like @theagentd. Smiley

I implemented a simple runtime JIT code generator using DynASM, which currently supports Matrix4f.mul(Vector4f), making use of SSE instructions (movaps, shufps, mulps and addps).

The astonishing results are: Even with JNI overhead this function is 8% faster than the corresponding scalar Java code on an i3-2120...
(I did a benchmark with 100 million invocations). Resulting numbers were the same, but the JNI/SSE version was faster.

So JOML is going SSE and will get an optional acceleration JNI library!
Offline Riven
Administrator

« JGO Overlord »


Medals: 1371
Projects: 4
Exp: 16 years


Hand over your head.


« Reply #112 - Posted 2015-07-14 09:45:01 »

The real wins should be in bulk-operations within one JNI call - any numbers on that?

Hi, appreciate more people! Σ ♥ = ¾
Learn how to award medals... and work your way up the social rankings!
Offline KaiHH

JGO Kernel


Medals: 794



« Reply #113 - Posted 2015-07-14 09:47:45 »

Yepp. That's what I am after in the long run, and why I am using DynASM runtime code-generation. But I am really amazed now, how much faster even a single non-batched operation is.
I will update here when I get to implement the batching soon. Smiley
Offline princec

« JGO Spiffy Duke »


Medals: 1146
Projects: 3
Exp: 20 years


Eh? Who? What? ... Me?


« Reply #114 - Posted 2015-07-14 09:51:22 »

If you go that route remember you'll be needing to provide precompiled binaries for x86 and amd64, for Windows, Linux and MacOS Smiley

Cas Smiley

Offline Riven
Administrator

« JGO Overlord »


Medals: 1371
Projects: 4
Exp: 16 years


Hand over your head.


« Reply #115 - Posted 2015-07-14 09:52:19 »

Don't forget ARM6 and ARM7, as this might be valuable on Android Pointing

Hi, appreciate more people! Σ ♥ = ¾
Learn how to award medals... and work your way up the social rankings!
Offline theagentd
« Reply #116 - Posted 2015-07-14 10:53:49 »

Something that would maximize the throughput would be the ability to queue up get()s as well. In my case I will only be doing 3 functions:
matrix.translationRotateScale(...).mul(...).get(directBuffer);

This tiny code snippet might be run 100 000 times per frame though. That's still 100 000 JNI calls. If I could simply queue up the get()s as well, we could get away with 1 JNI call at the end for each thread instead. Maybe it should be possible for the user to create the queue and then pass it in as an argument to one or more NativeMatrix4 or whatever it'll be called. So it'd basically be something like this:

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
//Initialization
for(int i = 0; i < numThreads; i++){
    queues[i] = new Queue();
    matrices[i] = new NativeMatrix4f(queues[i]);
}

//Usage:
NativeMatrix4f matrix = matrices[threadID];
for(int i = ...; ...){
    matrix.translationRotateScale(...).mul(...).get(directBuffer);
}
queues[threadID].execute();

//At the end:
for(int i = 0; i < numThreads; i++){
    queues[i].dispose();
}



EDIT: I have a feeling that for the arguments to translationRotateScale() to work they'd also need to use special classes?

Myomyomyo.
Offline KaiHH

JGO Kernel


Medals: 794



« Reply #117 - Posted 2015-07-14 12:19:46 »

Thanks for your hints about your usage scenarios!
These are really helpful for me, since I can then anticipate a possible solution to satisfy those.
The more I play with this the more I want the "NativeMatrix4f" class to be as lightweight and close-to-the-metal as possible.
My current favourite solution is like this:
- the client only works with direct ByteBuffers/FloatBuffers (possibly hidden by that NativeMatrix4f)
- the NativeMatrix4f accepts as a constructor argument the ByteBuffer also with a possible 16-byte aligned offset into that buffer
- this would allow the client to have the best possible control over allocations and would allow JOML to perform the best possible computations
- native code does not allocate any memory representing a vector or matrix (it just allocates and writes executable memory for the JIT'ted functions)
- the client has control over the JIT: It can give it an opcodes ByteBuffer and JOML provides an API to construct the native function based on that
- again, the NativeMatrix4f or another class would provide a simple builder interface for constructing those native functions based on API calls on that matrix object
- I would now not mark any method as "terminal operation" but provide a public terminate() method on those builder classes, which the client can then invoke
- this means that, yes, we can make the get(Buffer) methods also lazy.
- so in the end we would have many "streams"/sequences of possible operations operating on matrices, vectors and quaternions, which are presented as direct ByteBuffer/FloatBuffers
- the API of the builder classes can remain the same like they are now (including translationRotateScale). Those would only "pack" the arguments into an "arguments" ByteBuffer using some defined protocol
- if the client does not wish to use those builder classes, it can of course always generate the arguments ByteBuffer itself
Offline KaiHH

JGO Kernel


Medals: 794



« Reply #118 - Posted 2015-07-14 16:35:57 »

Having hand-written (surely suboptimal) SSE code for 4x4 matrix multiplication in place, I did a benchmark with a bulk operation of 100 matrix multiplications.
The bulk was executed 1000 times and compared to a loop of 100,000 iterations with classic Matrix4f.mul(Matrix4f).
The result was: 12045.817 µs classic JOML versus 2639.805 µs JNI/SSE. Smiley
So an increase of almost 360% faster!
There is however a very very delicate limit on the size of generated code to stay in L1 cache, it seems.
If we do more than those 100 bulk matrix operations, the performance goes down to around 150% faster, because I emit the 100 matrix multiplications linearly in memory (which becomes quite big), instead of (what I should be doing) emitting the code of each operation just once and then unconditionally jump for each operation into the relevant code for that operation.
I will do that now.
Offline theagentd
« Reply #119 - Posted 2015-07-14 16:49:46 »

Hmm. I was just going to say that unless there's a significant gain from adding this kind of complexity it would not be worth it. I've tried libraries like Riven's MappedObject for better memory locality (although not for WSW), and even though the performance gains were in some case very significant (sometimes 3x) it was simply not worth it due to the added complexity of doing pretty much anything. A 4.5x faster matrix multiplication method would however be nice to have, but the added complexity has to be optional. I wouldn't mind spending some time to implement native matrices for my skeleton animation for example, but I'm not going to want to do the same for every single view matrix calculation I have. As an additional high-performance alternative for performance critical parts like bone calculations it does however sound worth it.

How does the performance of 4x3 "matrix multiplications" look with SSE? Would other methods be possible to optimize with such instructions as well? Specifically the performance of translationRotateScale() would be interesting for me, but there might be others that are of use for others as well. Might not be worth looking into too much though, but if you got the matrix multiplications 350% faster translationRotateScale() may end up being the bottleneck for me in the end.

Your previous post was a bit confusing to me. If I understood it right, you're saying that I could create a small native function by giving JOML an opcode buffer which is then somehow compiled to a native function? Huh So... Software compute shaders for Java? O_o

Oh, and one more thing. There's an annoying quirk when working with GLSL. A mat4x3 is a pretty bad GLSL type as it is treated as 4 vec3s by the compiler, which when working with uniforms has the same cost as 4 vec4s. A mat3x4 however is however 3 vec4s, which only counts as 3 uniforms. For this reason, I actually store my 4x3 skinning matrices transposed in a texture buffer and load them using three texture lookups each like this:
1  
2  
3  
4  
5  
6  
   int index = offset + boneIndex*3;
   return transpose(mat3x4(
         texelFetch(sampler, index+0),
         texelFetch(sampler, index+1),
         texelFetch(sampler, index+2)
   ) * weight);

The thing is that the transpose() function in GLSL is completely optimized away by all compilers, so this kind of packing has no overhead in the shader.

What I'm getting at is that it'd be useful to have functions that can store a matrix in a buffer as both a 4x4 and a 4x3 matrix, AND transposed versions of those as well.

Myomyomyo.
Pages: 1 2 3 [4] 5 6 ... 13
  ignore  |  Print  
 
 

 
Riven (521 views)
2019-09-04 15:33:17

hadezbladez (5476 views)
2018-11-16 13:46:03

hadezbladez (2379 views)
2018-11-16 13:41:33

hadezbladez (5737 views)
2018-11-16 13:35:35

hadezbladez (1215 views)
2018-11-16 13:32:03

EgonOlsen (4657 views)
2018-06-10 19:43:48

EgonOlsen (5665 views)
2018-06-10 19:43:44

EgonOlsen (3192 views)
2018-06-10 19:43:20

DesertCoockie (4091 views)
2018-05-13 18:23:11

nelsongames (5030 views)
2018-04-24 18:15:36
A NON-ideal modular configuration for Eclipse with JavaFX
by philfrei
2019-12-19 19:35:12

Java Gaming Resources
by philfrei
2019-05-14 16:15:13

Deployment and Packaging
by philfrei
2019-05-08 15:15:36

Deployment and Packaging
by philfrei
2019-05-08 15:13:34

Deployment and Packaging
by philfrei
2019-02-17 20:25:53

Deployment and Packaging
by mudlee
2018-08-22 18:09:50

Java Gaming Resources
by gouessej
2018-08-22 08:19:41

Deployment and Packaging
by gouessej
2018-08-22 08:04:08
java-gaming.org is not responsible for the content posted by its members, including references to external websites, and other references that may or may not have a relation with our primarily gaming and game production oriented community. inquiries and complaints can be sent via email to the info‑account of the company managing the website of java‑gaming.org
Powered by MySQL Powered by PHP Powered by SMF 1.1.18 | SMF © 2013, Simple Machines | Managed by Enhanced Four Valid XHTML 1.0! Valid CSS!